From 52deb65f082ad7e4a5d8951b1ede2ae827e0efe2 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 29 Mar 2024 14:09:30 +0800 Subject: [PATCH 01/99] CP-48666: initialize a skeleton project for Go SDK Signed-off-by: Luca Zhang --- Makefile | 8 +++- ocaml/sdk-gen/go/README.md | 74 ++++++++++++++++++++++++++++++ ocaml/sdk-gen/go/autogen/dune | 24 ++++++++++ ocaml/sdk-gen/go/dune | 10 ++++ ocaml/sdk-gen/go/gen_go_binding.ml | 16 +++++++ 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 ocaml/sdk-gen/go/README.md create mode 100644 ocaml/sdk-gen/go/autogen/dune create mode 100644 ocaml/sdk-gen/go/dune create mode 100644 ocaml/sdk-gen/go/gen_go_binding.ml diff --git a/Makefile b/Makefile index 991ce87c812..b5a081ff4f0 100644 --- a/Makefile +++ b/Makefile @@ -97,22 +97,26 @@ sdk: ocaml/sdk-gen/c/gen_c_binding.exe \ ocaml/sdk-gen/csharp/gen_csharp_binding.exe \ ocaml/sdk-gen/java/main.exe \ - ocaml/sdk-gen/powershell/gen_powershell_binding.exe + ocaml/sdk-gen/powershell/gen_powershell_binding.exe \ + ocaml/sdk-gen/go/gen_go_binding.exe dune build --profile=$(PROFILE) -f\ @ocaml/sdk-gen/c/generate \ @ocaml/sdk-gen/csharp/generate \ @ocaml/sdk-gen/java/generate \ - @ocaml/sdk-gen/powershell/generate + @ocaml/sdk-gen/powershell/generate \ + @ocaml/sdk-gen/go/generate rm -rf $(XAPISDK) mkdir -p $(XAPISDK)/c mkdir -p $(XAPISDK)/csharp mkdir -p $(XAPISDK)/java mkdir -p $(XAPISDK)/powershell mkdir -p $(XAPISDK)/python + mkdir -p $(XAPISDK)/go cp -r _build/default/ocaml/sdk-gen/c/autogen/* $(XAPISDK)/c cp -r _build/default/ocaml/sdk-gen/csharp/autogen/* $(XAPISDK)/csharp cp -r _build/default/ocaml/sdk-gen/java/autogen/* $(XAPISDK)/java cp -r _build/default/ocaml/sdk-gen/powershell/autogen/* $(XAPISDK)/powershell + cp -r _build/default/ocaml/sdk-gen/go/autogen/* $(XAPISDK)/go cp scripts/examples/python/XenAPI/XenAPI.py $(XAPISDK)/python sh ocaml/sdk-gen/windows-line-endings.sh $(XAPISDK)/csharp sh ocaml/sdk-gen/windows-line-endings.sh $(XAPISDK)/powershell diff --git a/ocaml/sdk-gen/go/README.md b/ocaml/sdk-gen/go/README.md new file mode 100644 index 00000000000..c8ba2a4d62b --- /dev/null +++ b/ocaml/sdk-gen/go/README.md @@ -0,0 +1,74 @@ +# XenServer SDK for Go + +Copyright (c) 2023-2024 Cloud Software Group, Inc. All Rights Reserved. + +XenServer SDK for Go is a complete SDK for XenServer, exposing the XenServer +API as Go module. It is written in Go. + +XenServer SDK for Go includes a struct for every API class, and a method for each API +call, so API documentation and examples written for other languages will apply +equally well to Go. In particular, the SDK Guide and the Management API Guide +are ideal for developers wishing to use XenServer SDK for Go. + +XenServer SDK for Go is free software. You can redistribute and modify it under the +terms of the BSD 2-Clause license. See LICENSE.txt for details. + +## Reference + +For XenServer documentation see + +The XenServer Management API Reference is available at + + +The XenServer Software Development Kit Guide is available at + + +A number of examples to help you get started with the SDK is available at + + +For community content, blogs, and downloads, visit + and + +To network with other developers using XenServer visit + + +## Prerequisites + +This library requires Go 1.22 or greater. + +## Folder Structure + +This archive contains the following folders that are relevant to Go developers: + +- `XenServerGo\src`: contains the Go source files can be used as the local module in a Go project. + +## Getting Started + +Extract the contents of this archive. + +A. To set up the local go module: + + 1. Create a new folder in your Go project, eg. `XenServerGo` + 2. Copy all files in `XenServerGo\src` to the new folder + +B. To use the XenServer module for Go in your Go project: + + 1. Add the following lines to your go.mod file: + + ``` + replace /XenServerGo => ./XenServerGo + ``` + + 2. Run the command: + + ``` + go mod tidy + ``` + + 3. Use the XenServer module for Go in file as follows: + + ``` + import ( + xenapi "/XenServerGo" + ) + ``` diff --git a/ocaml/sdk-gen/go/autogen/dune b/ocaml/sdk-gen/go/autogen/dune new file mode 100644 index 00000000000..c1cb1ddd3b8 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/dune @@ -0,0 +1,24 @@ +(rule + (targets LICENSE) + (deps + ../../LICENSE + ) + (action (copy %{deps} %{targets})) +) + +(rule + (targets README) + (deps + ../README.md + ) + (action (copy %{deps} %{targets})) +) + +(alias + (name generate) + (deps + LICENSE + README + (source_tree .) + ) +) diff --git a/ocaml/sdk-gen/go/dune b/ocaml/sdk-gen/go/dune new file mode 100644 index 00000000000..c6cc2c5b87c --- /dev/null +++ b/ocaml/sdk-gen/go/dune @@ -0,0 +1,10 @@ +(executable + (modes exe) + (name gen_go_binding) + (modules gen_go_binding) + (libraries + CommonFunctions + mustache + xapi-datamodel + ) +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml new file mode 100644 index 00000000000..76f1e1e4a79 --- /dev/null +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -0,0 +1,16 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) + +let main () = print_string "init project" + +let _ = main () From 526bb8a47309dbc73017065af0f6bfd89ec0f34f Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 29 Mar 2024 15:02:10 +0800 Subject: [PATCH 02/99] CP-47347: Add mustache template for Enum Types CP-47350: Add mustache template for Record/Ref/Class Types CP-47365: Add mustache template for API messages/errors CP-47363: Add mustache template for file header Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/templates/APIErrors.mustache | 6 +++ .../sdk-gen/go/templates/APIMessages.mustache | 6 +++ ocaml/sdk-gen/go/templates/Enum.mustache | 11 +++++ .../sdk-gen/go/templates/FileHeader.mustache | 11 +++++ ocaml/sdk-gen/go/templates/Record.mustache | 41 +++++++++++++++++++ 5 files changed, 75 insertions(+) create mode 100644 ocaml/sdk-gen/go/templates/APIErrors.mustache create mode 100644 ocaml/sdk-gen/go/templates/APIMessages.mustache create mode 100644 ocaml/sdk-gen/go/templates/Enum.mustache create mode 100644 ocaml/sdk-gen/go/templates/FileHeader.mustache create mode 100644 ocaml/sdk-gen/go/templates/Record.mustache diff --git a/ocaml/sdk-gen/go/templates/APIErrors.mustache b/ocaml/sdk-gen/go/templates/APIErrors.mustache new file mode 100644 index 00000000000..8128b5ef185 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/APIErrors.mustache @@ -0,0 +1,6 @@ +const ( +{{#api_errors}} + // + ERR_{{name}} = "{{name}}" +{{/api_errors}} +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/APIMessages.mustache b/ocaml/sdk-gen/go/templates/APIMessages.mustache new file mode 100644 index 00000000000..172e4e416c4 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/APIMessages.mustache @@ -0,0 +1,6 @@ +const ( +{{#api_messages}} + // + MSG_{{name}} = "{{name}}" +{{/api_messages}} +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/Enum.mustache b/ocaml/sdk-gen/go/templates/Enum.mustache new file mode 100644 index 00000000000..1b668dd19bc --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Enum.mustache @@ -0,0 +1,11 @@ +{{#enums}} +type {{name}} string + +const ( +{{#values}} + //{{#doc}} {{.}}{{/doc}} + {{name}} {{type}} = "{{value}}" +{{/values}} +) + +{{/enums}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/FileHeader.mustache b/ocaml/sdk-gen/go/templates/FileHeader.mustache new file mode 100644 index 00000000000..a21900ec1c9 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/FileHeader.mustache @@ -0,0 +1,11 @@ +package xenapi +{{#modules}} +{{#import}} + +import ( +{{#items}} + {{#sname}}{{.}} {{/sname}}"{{name}}" +{{/items}} +) +{{/import}} +{{/modules}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/Record.mustache b/ocaml/sdk-gen/go/templates/Record.mustache new file mode 100644 index 00000000000..e4d874ba978 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Record.mustache @@ -0,0 +1,41 @@ +type {{name}}Record struct { +{{#fields}} + //{{#description}} {{.}}{{/description}} + {{name}} {{type}} +{{/fields}} +} + +type {{name}}Ref string + +{{#event}} +type RecordInterface interface{} + +type EventBatch struct { + Token string + ValidRefCounts map[string]int + Events []EventRecord +} + +{{/event}} +{{#description}} +// {{.}} +{{/description}} +{{#session}} +type {{name}}Class struct { + client *rpcClient + ref SessionRef +} + +func NewSession(opts *ClientOpts) *SessionClass { + client := NewJsonRPCClient(opts) + var session SessionClass + session.client = client + + return &session +} +{{/session}} +{{^session}} +type {{name}}Class struct{} + +var {{name}} *{{name}}Class +{{/session}} \ No newline at end of file From 2aa425580e55c9b6382bf7631b693a49fb185fcc Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 29 Mar 2024 17:10:52 +0800 Subject: [PATCH 03/99] CP-47351: generate Record and Ref Type Golang code for all classes Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/dune | 31 ++++- ocaml/sdk-gen/go/gen_go_binding.ml | 12 +- ocaml/sdk-gen/go/gen_go_binding.mli | 12 ++ ocaml/sdk-gen/go/gen_go_helper.ml | 167 +++++++++++++++++++++++ ocaml/sdk-gen/go/gen_go_helper.mli | 28 ++++ ocaml/sdk-gen/go/test_data/record.go | 22 +++ ocaml/sdk-gen/go/test_gen_go.ml | 196 +++++++++++++++++++++++++++ ocaml/sdk-gen/go/test_gen_go.mli | 12 ++ 8 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 ocaml/sdk-gen/go/gen_go_binding.mli create mode 100644 ocaml/sdk-gen/go/gen_go_helper.ml create mode 100644 ocaml/sdk-gen/go/gen_go_helper.mli create mode 100644 ocaml/sdk-gen/go/test_data/record.go create mode 100644 ocaml/sdk-gen/go/test_gen_go.ml create mode 100644 ocaml/sdk-gen/go/test_gen_go.mli diff --git a/ocaml/sdk-gen/go/dune b/ocaml/sdk-gen/go/dune index c6cc2c5b87c..f481b389530 100644 --- a/ocaml/sdk-gen/go/dune +++ b/ocaml/sdk-gen/go/dune @@ -6,5 +6,34 @@ CommonFunctions mustache xapi-datamodel + gen_go_helper ) -) \ No newline at end of file +) + +(library + (name gen_go_helper) + (modules gen_go_helper) + (libraries + CommonFunctions + mustache + xapi-datamodel + ) +) + +(rule + (alias generate) + (deps + (:x gen_go_binding.exe) + (source_tree templates) + ) + (action (run %{x})) +) + +(test + (name test_gen_go) + (modules test_gen_go) + (libraries alcotest xapi-test-utils gen_go_helper) + (deps + (source_tree test_data) + ) +) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 76f1e1e4a79..378d58e53ea 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -11,6 +11,16 @@ GNU Lesser General Public License for more details. *) -let main () = print_string "init project" +open Gen_go_helper + +let main () = + let objects = Json.xenapi objects in + List.iter + (fun (name, obj) -> + let record_rendered = render_template "Record.mustache" obj in + let output_file = name ^ ".go" in + generate_file record_rendered output_file + ) + objects let _ = main () diff --git a/ocaml/sdk-gen/go/gen_go_binding.mli b/ocaml/sdk-gen/go/gen_go_binding.mli new file mode 100644 index 00000000000..40ffdaa7dfa --- /dev/null +++ b/ocaml/sdk-gen/go/gen_go_binding.mli @@ -0,0 +1,12 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml new file mode 100644 index 00000000000..ce207ed0322 --- /dev/null +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -0,0 +1,167 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) + +(* Generator of Go bindings from the datamodel *) + +open Datamodel_types +open Datamodel_utils +open Dm_api +open CommonFunctions + +let dest_dir = "autogen" + +let templates_dir = "templates" + +let ( // ) = Filename.concat + +let src_dir = dest_dir // "src" + +let snake_to_camel (s : string) : string = + Astring.String.cuts ~sep:"_" s + |> List.map (fun s -> Astring.String.cuts ~sep:"-" s) + |> List.concat + |> List.map String.capitalize_ascii + |> String.concat "" + +let render_template template_file json = + let templ = + string_of_file (templates_dir // template_file) |> Mustache.of_string + in + Mustache.render templ json + +let generate_file rendered output_file = + let out_chan = open_out (src_dir // output_file) in + Fun.protect + (fun () -> output_string out_chan rendered) + ~finally:(fun () -> close_out out_chan) + +module Json = struct + let rec string_of_ty ty = + match ty with + | SecretString | String -> + "string" + | Int -> + "int" + | Float -> + "float64" + | Bool -> + "bool" + | DateTime -> + "time.Time" + | Enum (name, _kv) -> + snake_to_camel name + | Set ty -> + "[]" ^ string_of_ty ty + | Map (ty1, ty2) -> + let s1 = string_of_ty ty1 in + let s2 = string_of_ty ty2 in + "map[" ^ s1 ^ "]" ^ s2 + | Ref r -> + snake_to_camel r ^ "Ref" + | Record r -> + snake_to_camel r ^ "Record" + | Option ty -> + string_of_ty ty + + let fields_of_obj obj = + let rec flatten_contents contents = + List.fold_left + (fun l -> function + | Field f -> + f :: l + | Namespace (_name, contents) -> + flatten_contents contents @ l + ) + [] contents + in + let fields = flatten_contents obj.contents in + let concat_and_convert field = + let concated = + String.concat "" (List.map snake_to_camel field.full_name) + in + match concated with + | "Uuid" | "Id" -> + String.uppercase_ascii concated + | _ -> + concated + in + List.map + (fun field -> + let ty = string_of_ty field.ty in + `O + [ + ("name", `String (concat_and_convert field)) + ; ("description", `String (String.trim field.field_description)) + ; ("type", `String ty) + ] + ) + fields + + let xenapi objs = + List.map + (fun obj -> + let fields = fields_of_obj obj in + let event_snapshot = + if String.lowercase_ascii obj.name = "event" then + [ + `O + [ + ("name", `String "Snapshot") + ; ( "description" + , `String + "The record of the database object that was added, \ + changed or deleted" + ) + ; ("type", `String "RecordInterface") + ] + ] + else + [] + in + let obj_name = snake_to_camel obj.name in + let event_session_value = function + | "event" -> + [("event", `Bool true); ("session", `Null)] + | "session" -> + [("event", `Null); ("session", `Bool true)] + | _ -> + [("event", `Null); ("session", `Null)] + in + let base_assoc_list = + [ + ("name", `String obj_name) + ; ("description", `String (String.trim obj.description)) + ; ("fields", `A (event_snapshot @ fields)) + ] + in + let assoc_list = event_session_value obj.name @ base_assoc_list in + (String.lowercase_ascii obj.name, `O assoc_list) + ) + objs +end + +let objects = + let api = Datamodel.all_api in + (* Add all implicit messages *) + let api = add_implicit_messages api in + (* Only include messages that are visible to a XenAPI client *) + let api = filter (fun _ -> true) (fun _ -> true) on_client_side api in + (* And only messages marked as not hidden from the docs, and non-internal fields *) + let api = + filter + (fun _ -> true) + (fun f -> not f.internal_only) + (fun m -> not m.msg_hide_from_docs) + api + in + objects_of_api api diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli new file mode 100644 index 00000000000..b88d0348498 --- /dev/null +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -0,0 +1,28 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) + +open Datamodel_types + +val ( // ) : string -> string -> string + +val snake_to_camel : string -> string + +val objects : obj list + +val render_template : string -> Mustache.Json.t -> string + +val generate_file : string -> string -> unit + +module Json : sig + val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list +end diff --git a/ocaml/sdk-gen/go/test_data/record.go b/ocaml/sdk-gen/go/test_data/record.go new file mode 100644 index 00000000000..fd5908e19c0 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/record.go @@ -0,0 +1,22 @@ +type SessionRecord struct { + // Unique identifier/object reference + UUID string + // Currently connected host + ThisHost HostRef +} + +type SessionRef string + +// A session +type SessionClass struct { + client *rpcClient + ref SessionRef +} + +func NewSession(opts *ClientOpts) *SessionClass { + client := NewJsonRPCClient(opts) + var session SessionClass + session.client = client + + return &session +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml new file mode 100644 index 00000000000..1a435311976 --- /dev/null +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -0,0 +1,196 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) + +open Test_highlevel +open CommonFunctions +open Gen_go_helper + +let test_data_dir = "test_data" + +let string_of_file filename = + string_of_file (test_data_dir // filename) |> String.trim + +let check_true str = Alcotest.(check bool) str true + +module SnakeToCamelTest = Generic.MakeStateless (struct + module Io = struct + type input_t = string + + type output_t = string + + let string_of_input_t = Test_printers.string + + let string_of_output_t = Test_printers.string + end + + let transform = snake_to_camel + + let tests = + `QuickAndAutoDocumented + [ + ("ni_hao-Nanjin", "NiHaoNanjin") + ; ("ni_hao", "NiHao") + ; ("nanjing", "Nanjing") + ] +end) + +let rec is_same_struct_of_value (value1 : Mustache.Json.value) + (value2 : Mustache.Json.value) = + match (value1, value2) with + | `Null, _ | _, `Null -> + true + | `Bool _, `Bool _ -> + true + | `String _, `String _ -> + true + | `Float _, `Float _ -> + true + | `O o1, `O o2 -> + let keys1 = List.sort compare (List.map fst o1) in + let keys2 = List.sort compare (List.map fst o2) in + if keys1 <> keys2 then + false + else + List.for_all + (fun key -> + is_same_struct_of_value (List.assoc key o1) (List.assoc key o2) + ) + keys1 + | `A [], `A [] -> + true + | `A [], `A (x :: xs) -> + List.for_all (fun obj -> is_same_struct_of_value x obj) xs + | `A (x :: xs), `A ys -> + List.for_all (fun obj -> is_same_struct_of_value x obj) (xs @ ys) + | _ -> + false + +let is_same_struct (obj1 : Mustache.Json.t) (obj2 : Mustache.Json.t) = + match (obj1, obj2) with + | `O o1, `O o2 -> + let keys1 = List.sort compare (List.map fst o1) in + let keys2 = List.sort compare (List.map fst o2) in + if keys1 <> keys2 then + false + else + List.for_all + (fun key -> + is_same_struct_of_value (List.assoc key o1) (List.assoc key o2) + ) + keys1 + | `A [], `A [] -> + true + | `A [], `A (x :: xs) -> + List.for_all (fun obj -> is_same_struct_of_value x obj) xs + | `A (x :: xs), `A ys -> + List.for_all (fun obj -> is_same_struct_of_value x obj) (xs @ ys) + | _ -> + false + +let rec string_of_json_value (value : Mustache.Json.value) : string = + match value with + | `Null -> + "null" + | `Bool b -> + string_of_bool b + | `Float f -> + string_of_float f + | `String s -> + "\"" ^ s ^ "\"" + | `A arr -> + "[" ^ String.concat ", " (List.map string_of_json_value arr) ^ "]" + | `O obj -> + "{" + ^ String.concat ", " + (List.map + (fun (k, v) -> "\"" ^ k ^ "\": " ^ string_of_json_value v) + obj + ) + ^ "}" + +let string_of_json (json : Mustache.Json.t) : string = + match json with + | `A arr -> + "[" ^ String.concat ", " (List.map string_of_json_value arr) ^ "]" + | `O obj -> + "{" + ^ String.concat ", " + (List.map + (fun (k, v) -> "\"" ^ k ^ "\": " ^ string_of_json_value v) + obj + ) + ^ "}" + +let record : Mustache.Json.t = + `O + [ + ("name", `String "Session") + ; ("description", `String "A session") + ; ("session", `Bool true) + ; ("event", `Bool false) + ; ( "fields" + , `A + [ + `O + [ + ("name", `String "UUID") + ; ("description", `String "Unique identifier/object reference") + ; ("type", `String "string") + ] + ; `O + [ + ("name", `String "ThisHost") + ; ("description", `String "Currently connected host") + ; ("type", `String "HostRef") + ] + ] + ) + ] + +module TemplatesTest = Generic.MakeStateless (struct + module Io = struct + type input_t = string * Mustache.Json.t + + type output_t = string + + let string_of_input_t (template, json) = + "The template is " ^ template ^ " with json: " ^ string_of_json json + + let string_of_output_t = Test_printers.string + end + + let transform (template, json) = render_template template json |> String.trim + + let record_rendered = string_of_file "record.go" + + let tests = + `QuickAndAutoDocumented [(("Record.mustache", record), record_rendered)] +end) + +let generated_json_tests = + let records () = + let objects = Json.xenapi objects in + check_true "Mustache.Json of records has right structure" + @@ List.for_all (fun (_, obj) -> is_same_struct obj record) objects + in + [("records", `Quick, records)] + +let tests = + make_suite "gen_go_binding_" + [ + ("snake_to_camel", SnakeToCamelTest.tests) + ; ("templates", TemplatesTest.tests) + ; ("generated_mustache_jsons", generated_json_tests) + ] + +let () = Alcotest.run "Gen go binding" tests diff --git a/ocaml/sdk-gen/go/test_gen_go.mli b/ocaml/sdk-gen/go/test_gen_go.mli new file mode 100644 index 00000000000..40ffdaa7dfa --- /dev/null +++ b/ocaml/sdk-gen/go/test_gen_go.mli @@ -0,0 +1,12 @@ +(* Copyright (c) Cloud Software Group, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; version 2.1 only. with the special + exception on linking described in file LICENSE. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. +*) From 49a22b0ef735e34674e0fed0c0492b7ffcb442ac Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 29 Mar 2024 18:18:47 +0800 Subject: [PATCH 04/99] CP-47348: generate Golang code of Enum Type for all classes Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 4 +- ocaml/sdk-gen/go/gen_go_helper.ml | 151 +++++++++++++++++++++++------ ocaml/sdk-gen/go/test_data/enum.go | 8 ++ ocaml/sdk-gen/go/test_gen_go.ml | 55 ++++++++++- 4 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/enum.go diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 378d58e53ea..677778f26d5 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -17,9 +17,11 @@ let main () = let objects = Json.xenapi objects in List.iter (fun (name, obj) -> + let enums_rendered = render_template "Enum.mustache" obj in let record_rendered = render_template "Record.mustache" obj in + let rendered = enums_rendered ^ record_rendered in let output_file = name ^ ".go" in - generate_file record_rendered output_file + generate_file rendered output_file ) objects diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index ce207ed0322..d45bb2821dc 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -45,35 +45,65 @@ let generate_file rendered output_file = (fun () -> output_string out_chan rendered) ~finally:(fun () -> close_out out_chan) +module EnumSet = Set.Make (struct + type t = string * (string * string) list + + let compare (x0, _y0) (x1, _y1) = String.compare x0 x1 +end) + module Json = struct - let rec string_of_ty ty = + type enum = (string * string) list + + module StringMap = Map.Make (String) + + type enums = enum StringMap.t + + let choose_enum _key a _b = Some a + + let rec string_of_ty_with_enums ty : string * enums = match ty with | SecretString | String -> - "string" + ("string", StringMap.empty) | Int -> - "int" + ("int", StringMap.empty) | Float -> - "float64" + ("float64", StringMap.empty) | Bool -> - "bool" + ("bool", StringMap.empty) | DateTime -> - "time.Time" - | Enum (name, _kv) -> - snake_to_camel name + ("time.Time", StringMap.empty) + | Enum (name, kv) -> + let name = snake_to_camel name in + (name, StringMap.singleton name kv) | Set ty -> - "[]" ^ string_of_ty ty + let s, e = string_of_ty_with_enums ty in + ("[]" ^ s, e) | Map (ty1, ty2) -> - let s1 = string_of_ty ty1 in - let s2 = string_of_ty ty2 in - "map[" ^ s1 ^ "]" ^ s2 + let s1, e1 = string_of_ty_with_enums ty1 in + let s2, e2 = string_of_ty_with_enums ty2 in + let ty = "map[" ^ s1 ^ "]" ^ s2 in + (ty, StringMap.union choose_enum e1 e2) | Ref r -> - snake_to_camel r ^ "Ref" + (snake_to_camel r ^ "Ref", StringMap.empty) | Record r -> - snake_to_camel r ^ "Record" + (snake_to_camel r ^ "Record", StringMap.empty) | Option ty -> - string_of_ty ty + string_of_ty_with_enums ty - let fields_of_obj obj = + let of_enum name vs = + let name = snake_to_camel name in + let of_value (v, d) = + `O + [ + ("value", `String v) + ; ("doc", `String d) + ; ("name", `String (name ^ snake_to_camel v)) + ; ("type", `String name) + ] + in + `O [("name", `String name); ("values", `A (List.map of_value vs))] + + let fields_of_obj_with_enums obj = let rec flatten_contents contents = List.fold_left (fun l -> function @@ -95,22 +125,88 @@ module Json = struct | _ -> concated in - List.map - (fun field -> - let ty = string_of_ty field.ty in - `O - [ - ("name", `String (concat_and_convert field)) - ; ("description", `String (String.trim field.field_description)) - ; ("type", `String ty) - ] + List.fold_left + (fun (fields, enums) field -> + let ty, e = string_of_ty_with_enums field.ty in + ( `O + [ + ("name", `String (concat_and_convert field)) + ; ("description", `String (String.trim field.field_description)) + ; ("type", `String ty) + ] + :: fields + , StringMap.union choose_enum enums e + ) + ) + ([], StringMap.empty) fields + + let enums_from_result obj msg = + match msg.msg_result with + | None -> + StringMap.empty + | Some (t, _d) -> + if obj.name = "event" && String.lowercase_ascii msg.msg_name = "from" + then + StringMap.empty + else + let _, enums = string_of_ty_with_enums t in + enums + + let enums_from_params ps = + List.fold_left + (fun enums p -> + let _t, e = string_of_ty_with_enums p.param_type in + StringMap.union choose_enum enums e + ) + StringMap.empty ps + + let session_id = + { + param_type= Ref Datamodel_common._session + ; param_name= "session_id" + ; param_doc= "Reference to a valid session" + ; param_release= Datamodel_common.rio_release + ; param_default= None + } + + let enums_in_messages_of_obj obj = + List.fold_left + (fun enums msg -> + let params = + if msg.msg_session then + session_id :: msg.msg_params + else + msg.msg_params + in + let enums1 = enums_from_result obj msg in + let enums2 = enums_from_params params in + enums + |> StringMap.union choose_enum enums1 + |> StringMap.union choose_enum enums2 ) - fields + StringMap.empty obj.messages let xenapi objs = + let enums_acc = ref StringMap.empty in + let erase_existed enums = + let enums = + StringMap.filter (fun k _ -> not (StringMap.mem k !enums_acc)) enums + in + enums_acc := StringMap.union choose_enum !enums_acc enums ; + enums + in List.map (fun obj -> - let fields = fields_of_obj obj in + let fields, enums_in_fields = fields_of_obj_with_enums obj in + let enums_in_msgs = enums_in_messages_of_obj obj in + let enums = + let enums = + enums_in_fields + |> StringMap.union choose_enum enums_in_msgs + |> erase_existed + in + StringMap.fold (fun k v acc -> of_enum k v :: acc) enums [] + in let event_snapshot = if String.lowercase_ascii obj.name = "event" then [ @@ -142,6 +238,7 @@ module Json = struct ("name", `String obj_name) ; ("description", `String (String.trim obj.description)) ; ("fields", `A (event_snapshot @ fields)) + ; ("enums", `A enums) ] in let assoc_list = event_session_value obj.name @ base_assoc_list in diff --git a/ocaml/sdk-gen/go/test_data/enum.go b/ocaml/sdk-gen/go/test_data/enum.go new file mode 100644 index 00000000000..0a0e17be7d3 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/enum.go @@ -0,0 +1,8 @@ +type VMTelemetryFrequency string + +const ( + // Run telemetry task daily + VMTelemetryFrequencyDaily VMTelemetryFrequency = "daily" + // Run telemetry task weekly + VMTelemetryFrequencyWeekly VMTelemetryFrequency = "weekly" +) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 1a435311976..e9e3cd017ea 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -157,6 +157,39 @@ let record : Mustache.Json.t = ) ] +let enums : Mustache.Json.t = + `O + [ + ( "enums" + , `A + [ + `O + [ + ("name", `String "VMTelemetryFrequency") + ; ( "values" + , `A + [ + `O + [ + ("value", `String "daily") + ; ("doc", `String "Run telemetry task daily") + ; ("name", `String "VMTelemetryFrequencyDaily") + ; ("type", `String "VMTelemetryFrequency") + ] + ; `O + [ + ("value", `String "weekly") + ; ("doc", `String "Run telemetry task weekly") + ; ("name", `String "VMTelemetryFrequencyWeekly") + ; ("type", `String "VMTelemetryFrequency") + ] + ] + ) + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -173,17 +206,31 @@ module TemplatesTest = Generic.MakeStateless (struct let record_rendered = string_of_file "record.go" + let enums_rendered = string_of_file "enum.go" + let tests = - `QuickAndAutoDocumented [(("Record.mustache", record), record_rendered)] + `QuickAndAutoDocumented + [ + (("Record.mustache", record), record_rendered) + ; (("Enum.mustache", enums), enums_rendered) + ] end) let generated_json_tests = - let records () = + let merge (obj1 : Mustache.Json.t) (obj2 : Mustache.Json.t) = + match (obj1, obj2) with + | `O list1, `O list2 -> + `O (list1 @ list2) + | _ -> + `O [] + in + let jsons () = + let json = merge record enums in let objects = Json.xenapi objects in check_true "Mustache.Json of records has right structure" - @@ List.for_all (fun (_, obj) -> is_same_struct obj record) objects + @@ List.for_all (fun (_, obj) -> is_same_struct obj json) objects in - [("records", `Quick, records)] + [("records", `Quick, jsons)] let tests = make_suite "gen_go_binding_" From 96d045c7b1ad7b0d79f86b8c4b4d36df2d02aab5 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 29 Mar 2024 19:31:10 +0800 Subject: [PATCH 05/99] CP-47362: generate file headers Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 3 ++- ocaml/sdk-gen/go/gen_go_helper.ml | 28 +++++++++++++++++++++-- ocaml/sdk-gen/go/test_data/file_header.go | 6 +++++ ocaml/sdk-gen/go/test_gen_go.ml | 27 +++++++++++++++++++--- 4 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/file_header.go diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 677778f26d5..2606be80ab7 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -17,9 +17,10 @@ let main () = let objects = Json.xenapi objects in List.iter (fun (name, obj) -> + let header_rendered = render_template "FileHeader.mustache" obj ^ "\n" in let enums_rendered = render_template "Enum.mustache" obj in let record_rendered = render_template "Record.mustache" obj in - let rendered = enums_rendered ^ record_rendered in + let rendered = header_rendered ^ enums_rendered ^ record_rendered in let output_file = name ^ ".go" in generate_file rendered output_file ) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index d45bb2821dc..a2ea4503e31 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -126,7 +126,10 @@ module Json = struct concated in List.fold_left - (fun (fields, enums) field -> + (fun (fields, enums, has_time_type) field -> + let is_time_type = + match field.ty with DateTime -> true | _ -> false + in let ty, e = string_of_ty_with_enums field.ty in ( `O [ @@ -136,9 +139,11 @@ module Json = struct ] :: fields , StringMap.union choose_enum enums e + , if is_time_type then true else has_time_type ) ) - ([], StringMap.empty) fields + ([], StringMap.empty, false) + fields let enums_from_result obj msg = match msg.msg_result with @@ -186,6 +191,23 @@ module Json = struct ) StringMap.empty obj.messages + let get_modules messages has_time_type = + match messages with + | [] -> + `Null + | _ -> + let items = + match has_time_type with + | true -> + [ + `O [("name", `String "fmt"); ("sname", `Null)] + ; `O [("name", `String "time"); ("sname", `Null)] + ] + | false -> + [`O [("name", `String "fmt"); ("sname", `Null)]] + in + `O [("import", `Bool true); ("items", `A items)] + let xenapi objs = let enums_acc = ref StringMap.empty in let erase_existed enums = @@ -225,6 +247,7 @@ module Json = struct [] in let obj_name = snake_to_camel obj.name in + let modules = get_modules obj.messages has_time_type in let event_session_value = function | "event" -> [("event", `Bool true); ("session", `Null)] @@ -239,6 +262,7 @@ module Json = struct ; ("description", `String (String.trim obj.description)) ; ("fields", `A (event_snapshot @ fields)) ; ("enums", `A enums) + ; ("modules", modules) ] in let assoc_list = event_session_value obj.name @ base_assoc_list in diff --git a/ocaml/sdk-gen/go/test_data/file_header.go b/ocaml/sdk-gen/go/test_data/file_header.go new file mode 100644 index 00000000000..a40c01a5eb8 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/file_header.go @@ -0,0 +1,6 @@ +package xenapi + +import ( + "fmt" + time1 "time" +) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index e9e3cd017ea..a93250ed3bc 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -157,6 +157,24 @@ let record : Mustache.Json.t = ) ] +let header : Mustache.Json.t = + `O + [ + ( "modules" + , `O + [ + ("import", `Bool true) + ; ( "items" + , `A + [ + `O [("name", `String "fmt"); ("sname", `Null)] + ; `O [("name", `String "time"); ("sname", `String "time1")] + ] + ) + ] + ) + ] + let enums : Mustache.Json.t = `O [ @@ -204,6 +222,8 @@ module TemplatesTest = Generic.MakeStateless (struct let transform (template, json) = render_template template json |> String.trim + let file_header_rendered = string_of_file "file_header.go" + let record_rendered = string_of_file "record.go" let enums_rendered = string_of_file "enum.go" @@ -211,7 +231,8 @@ module TemplatesTest = Generic.MakeStateless (struct let tests = `QuickAndAutoDocumented [ - (("Record.mustache", record), record_rendered) + (("FileHeader.mustache", header), file_header_rendered) + ; (("Record.mustache", record), record_rendered) ; (("Enum.mustache", enums), enums_rendered) ] end) @@ -225,12 +246,12 @@ let generated_json_tests = `O [] in let jsons () = - let json = merge record enums in + let json = enums |> merge record |> merge header in let objects = Json.xenapi objects in check_true "Mustache.Json of records has right structure" @@ List.for_all (fun (_, obj) -> is_same_struct obj json) objects in - [("records", `Quick, jsons)] + [("jsons", `Quick, jsons)] let tests = make_suite "gen_go_binding_" From 612b73b3f4dce372f8eda7413d507a45c5aed420 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 8 Apr 2024 20:31:49 +0800 Subject: [PATCH 06/99] CP-48666: collect api errors Signed-off-by: Luca Zhang --- ocaml/xapi-consts/api_errors.ml | 1242 ++++++++++++++++--------------- 1 file changed, 659 insertions(+), 583 deletions(-) diff --git a/ocaml/xapi-consts/api_errors.ml b/ocaml/xapi-consts/api_errors.ml index 43fff504a3d..5616ba3a1c5 100644 --- a/ocaml/xapi-consts/api_errors.ml +++ b/ocaml/xapi-consts/api_errors.ml @@ -13,6 +13,14 @@ *) exception Server_error of string * string list +let ( $ ) f x = f x + +let errors = ref [] + +let add_error error = + errors := error :: !errors ; + error + let to_string = function | Server_error (name, args) -> Printf.sprintf "Server_error(%s, [ %a ])" name @@ -29,933 +37,983 @@ let _ = None ) -let message_deprecated = "MESSAGE_DEPRECATED" +let message_deprecated = add_error "MESSAGE_DEPRECATED" -let message_removed = "MESSAGE_REMOVED" +let message_removed = add_error "MESSAGE_REMOVED" -let permission_denied = "PERMISSION_DENIED" +let permission_denied = add_error "PERMISSION_DENIED" -let internal_error = "INTERNAL_ERROR" +let internal_error = add_error "INTERNAL_ERROR" -let map_duplicate_key = "MAP_DUPLICATE_KEY" +let map_duplicate_key = add_error "MAP_DUPLICATE_KEY" -let db_uniqueness_constraint_violation = "DB_UNIQUENESS_CONSTRAINT_VIOLATION" +let db_uniqueness_constraint_violation = + add_error "DB_UNIQUENESS_CONSTRAINT_VIOLATION" -let location_not_unique = "LOCATION_NOT_UNIQUE" +let location_not_unique = add_error "LOCATION_NOT_UNIQUE" -let message_method_unknown = "MESSAGE_METHOD_UNKNOWN" +let message_method_unknown = add_error "MESSAGE_METHOD_UNKNOWN" -let message_parameter_count_mismatch = "MESSAGE_PARAMETER_COUNT_MISMATCH" +let message_parameter_count_mismatch = + add_error "MESSAGE_PARAMETER_COUNT_MISMATCH" -let value_not_supported = "VALUE_NOT_SUPPORTED" +let value_not_supported = add_error "VALUE_NOT_SUPPORTED" -let invalid_value = "INVALID_VALUE" +let invalid_value = add_error "INVALID_VALUE" -let memory_constraint_violation = "MEMORY_CONSTRAINT_VIOLATION" +let memory_constraint_violation = add_error "MEMORY_CONSTRAINT_VIOLATION" -let memory_constraint_violation_order = "MEMORY_CONSTRAINT_VIOLATION_ORDER" +let memory_constraint_violation_order = + add_error "MEMORY_CONSTRAINT_VIOLATION_ORDER" -let memory_constraint_violation_maxpin = "MEMORY_CONSTRAINT_VIOLATION_MAXPIN" +let memory_constraint_violation_maxpin = + add_error "MEMORY_CONSTRAINT_VIOLATION_MAXPIN" -let field_type_error = "FIELD_TYPE_ERROR" +let field_type_error = add_error "FIELD_TYPE_ERROR" -let session_authentication_failed = "SESSION_AUTHENTICATION_FAILED" +let session_authentication_failed = add_error "SESSION_AUTHENTICATION_FAILED" -let session_authorization_failed = "SESSION_AUTHORIZATION_FAILED" +let session_authorization_failed = add_error "SESSION_AUTHORIZATION_FAILED" -let session_invalid = "SESSION_INVALID" +let session_invalid = add_error "SESSION_INVALID" -let change_password_rejected = "CHANGE_PASSWORD_REJECTED" +let change_password_rejected = add_error "CHANGE_PASSWORD_REJECTED" -let user_is_not_local_superuser = "USER_IS_NOT_LOCAL_SUPERUSER" +let user_is_not_local_superuser = add_error "USER_IS_NOT_LOCAL_SUPERUSER" -let cannot_contact_host = "CANNOT_CONTACT_HOST" +let cannot_contact_host = add_error "CANNOT_CONTACT_HOST" -let tls_connection_failed = "TLS_CONNECTION_FAILED" +let tls_connection_failed = add_error "TLS_CONNECTION_FAILED" -let not_supported_during_upgrade = "NOT_SUPPORTED_DURING_UPGRADE" +let not_supported_during_upgrade = add_error "NOT_SUPPORTED_DURING_UPGRADE" -let handle_invalid = "HANDLE_INVALID" +let handle_invalid = add_error "HANDLE_INVALID" -let uuid_invalid = "UUID_INVALID" +let uuid_invalid = add_error "UUID_INVALID" -let vm_hvm_required = "VM_HVM_REQUIRED" +let vm_hvm_required = add_error "VM_HVM_REQUIRED" -let vm_no_vcpus = "VM_NO_VCPUS" +let vm_no_vcpus = add_error "VM_NO_VCPUS" -let vm_toomany_vcpus = "VM_TOO_MANY_VCPUS" +let vm_toomany_vcpus = add_error "VM_TOO_MANY_VCPUS" -let vm_is_protected = "VM_IS_PROTECTED" +let vm_is_protected = add_error "VM_IS_PROTECTED" -let vm_is_immobile = "VM_IS_IMMOBILE" +let vm_is_immobile = add_error "VM_IS_IMMOBILE" -let vm_is_using_nested_virt = "VM_IS_USING_NESTED_VIRT" +let vm_is_using_nested_virt = add_error "VM_IS_USING_NESTED_VIRT" -let host_in_use = "HOST_IN_USE" +let host_in_use = add_error "HOST_IN_USE" -let host_in_emergency_mode = "HOST_IN_EMERGENCY_MODE" +let host_in_emergency_mode = add_error "HOST_IN_EMERGENCY_MODE" -let host_cannot_read_metrics = "HOST_CANNOT_READ_METRICS" +let host_cannot_read_metrics = add_error "HOST_CANNOT_READ_METRICS" -let host_disabled = "HOST_DISABLED" +let host_disabled = add_error "HOST_DISABLED" -let host_disabled_until_reboot = "HOST_DISABLED_UNTIL_REBOOT" +let host_disabled_until_reboot = add_error "HOST_DISABLED_UNTIL_REBOOT" -let host_not_disabled = "HOST_NOT_DISABLED" +let host_not_disabled = add_error "HOST_NOT_DISABLED" -let host_not_live = "HOST_NOT_LIVE" +let host_not_live = add_error "HOST_NOT_LIVE" -let host_is_live = "HOST_IS_LIVE" +let host_is_live = add_error "HOST_IS_LIVE" -let host_power_on_mode_disabled = "HOST_POWER_ON_MODE_DISABLED" +let host_power_on_mode_disabled = add_error "HOST_POWER_ON_MODE_DISABLED" -let host_not_enough_free_memory = "HOST_NOT_ENOUGH_FREE_MEMORY" +let host_not_enough_free_memory = add_error "HOST_NOT_ENOUGH_FREE_MEMORY" -let host_not_enough_pcpus = "HOST_NOT_ENOUGH_PCPUS" +let host_not_enough_pcpus = add_error "HOST_NOT_ENOUGH_PCPUS" -let no_hosts_available = "NO_HOSTS_AVAILABLE" +let no_hosts_available = add_error "NO_HOSTS_AVAILABLE" -let host_offline = "HOST_OFFLINE" +let host_offline = add_error "HOST_OFFLINE" -let host_cannot_destroy_self = "HOST_CANNOT_DESTROY_SELF" +let host_cannot_destroy_self = add_error "HOST_CANNOT_DESTROY_SELF" -let host_is_slave = "HOST_IS_SLAVE" +let host_is_slave = add_error "HOST_IS_SLAVE" -let host_name_invalid = "HOST_NAME_INVALID" +let host_name_invalid = add_error "HOST_NAME_INVALID" -let host_has_resident_vms = "HOST_HAS_RESIDENT_VMS" +let host_has_resident_vms = add_error "HOST_HAS_RESIDENT_VMS" -let hosts_failed_to_enable_caching = "HOSTS_FAILED_TO_ENABLE_CACHING" +let hosts_failed_to_enable_caching = add_error "HOSTS_FAILED_TO_ENABLE_CACHING" -let hosts_failed_to_disable_caching = "HOSTS_FAILED_TO_DISABLE_CACHING" +let hosts_failed_to_disable_caching = + add_error "HOSTS_FAILED_TO_DISABLE_CACHING" -let host_cannot_see_SR = "HOST_CANNOT_SEE_SR" +let host_cannot_see_SR = add_error "HOST_CANNOT_SEE_SR" (* Host errors which explain why the host is in emergency mode *) -let host_its_own_slave = "HOST_ITS_OWN_SLAVE" +let host_its_own_slave = add_error "HOST_ITS_OWN_SLAVE" -let host_still_booting = "HOST_STILL_BOOTING" +let host_still_booting = add_error "HOST_STILL_BOOTING" (* license *) -let host_has_no_management_ip = "HOST_HAS_NO_MANAGEMENT_IP" +let host_has_no_management_ip = add_error "HOST_HAS_NO_MANAGEMENT_IP" -let host_master_cannot_talk_back = "HOST_MASTER_CANNOT_TALK_BACK" +let host_master_cannot_talk_back = add_error "HOST_MASTER_CANNOT_TALK_BACK" -let host_unknown_to_master = "HOST_UNKNOWN_TO_MASTER" +let host_unknown_to_master = add_error "HOST_UNKNOWN_TO_MASTER" let host_xapi_version_higher_than_coordinator = - "HOST_XAPI_VERSION_HIGHER_THAN_COORDINATOR" + add_error "HOST_XAPI_VERSION_HIGHER_THAN_COORDINATOR" (* should be fenced *) -let host_broken = "HOST_BROKEN" +let host_broken = add_error "HOST_BROKEN" -let interface_has_no_ip = "INTERFACE_HAS_NO_IP" +let interface_has_no_ip = add_error "INTERFACE_HAS_NO_IP" -let invalid_ip_address_specified = "INVALID_IP_ADDRESS_SPECIFIED" +let invalid_ip_address_specified = add_error "INVALID_IP_ADDRESS_SPECIFIED" -let invalid_cidr_address_specified = "INVALID_CIDR_ADDRESS_SPECIFIED" +let invalid_cidr_address_specified = add_error "INVALID_CIDR_ADDRESS_SPECIFIED" -let address_violates_locking_constraint = "ADDRESS_VIOLATES_LOCKING_CONSTRAINT" +let address_violates_locking_constraint = + add_error "ADDRESS_VIOLATES_LOCKING_CONSTRAINT" -let pif_has_no_network_configuration = "PIF_HAS_NO_NETWORK_CONFIGURATION" +let pif_has_no_network_configuration = + add_error "PIF_HAS_NO_NETWORK_CONFIGURATION" -let pif_has_no_v6_network_configuration = "PIF_HAS_NO_V6_NETWORK_CONFIGURATION" +let pif_has_no_v6_network_configuration = + add_error "PIF_HAS_NO_V6_NETWORK_CONFIGURATION" -let device_attach_timeout = "DEVICE_ATTACH_TIMEOUT" +let device_attach_timeout = add_error "DEVICE_ATTACH_TIMEOUT" -let device_detach_timeout = "DEVICE_DETACH_TIMEOUT" +let device_detach_timeout = add_error "DEVICE_DETACH_TIMEOUT" -let device_detach_rejected = "DEVICE_DETACH_REJECTED" +let device_detach_rejected = add_error "DEVICE_DETACH_REJECTED" -let network_sriov_insufficient_capacity = "NETWORK_SRIOV_INSUFFICIENT_CAPACITY" +let network_sriov_insufficient_capacity = + add_error "NETWORK_SRIOV_INSUFFICIENT_CAPACITY" -let network_sriov_already_enabled = "NETWORK_SRIOV_ALREADY_ENABLED" +let network_sriov_already_enabled = add_error "NETWORK_SRIOV_ALREADY_ENABLED" -let network_sriov_enable_failed = "NETWORK_SRIOV_ENABLE_FAILED" +let network_sriov_enable_failed = add_error "NETWORK_SRIOV_ENABLE_FAILED" -let network_sriov_disable_failed = "NETWORK_SRIOV_DISABLE_FAILED" +let network_sriov_disable_failed = add_error "NETWORK_SRIOV_DISABLE_FAILED" -let network_incompatible_with_sriov = "NETWORK_INCOMPATIBLE_WITH_SRIOV" +let network_incompatible_with_sriov = + add_error "NETWORK_INCOMPATIBLE_WITH_SRIOV" let network_incompatible_with_vlan_on_bridge = - "NETWORK_INCOMPATIBLE_WITH_VLAN_ON_BRIDGE" + add_error "NETWORK_INCOMPATIBLE_WITH_VLAN_ON_BRIDGE" let network_incompatible_with_vlan_on_sriov = - "NETWORK_INCOMPATIBLE_WITH_VLAN_ON_SRIOV" + add_error "NETWORK_INCOMPATIBLE_WITH_VLAN_ON_SRIOV" -let network_incompatible_with_bond = "NETWORK_INCOMPATIBLE_WITH_BOND" +let network_incompatible_with_bond = add_error "NETWORK_INCOMPATIBLE_WITH_BOND" -let network_incompatible_with_tunnel = "NETWORK_INCOMPATIBLE_WITH_TUNNEL" +let network_incompatible_with_tunnel = + add_error "NETWORK_INCOMPATIBLE_WITH_TUNNEL" -let network_has_incompatible_sriov_pifs = "NETWORK_HAS_INCOMPATIBLE_SRIOV_PIFS" +let network_has_incompatible_sriov_pifs = + add_error "NETWORK_HAS_INCOMPATIBLE_SRIOV_PIFS" let network_has_incompatible_vlan_on_sriov_pifs = - "NETWORK_HAS_INCOMPATIBLE_VLAN_ON_SRIOV_PIFS" + add_error "NETWORK_HAS_INCOMPATIBLE_VLAN_ON_SRIOV_PIFS" -let operation_not_allowed = "OPERATION_NOT_ALLOWED" +let operation_not_allowed = add_error "OPERATION_NOT_ALLOWED" -let operation_blocked = "OPERATION_BLOCKED" +let operation_blocked = add_error "OPERATION_BLOCKED" -let network_already_connected = "NETWORK_ALREADY_CONNECTED" +let network_already_connected = add_error "NETWORK_ALREADY_CONNECTED" -let network_unmanaged = "NETWORK_UNMANAGED" +let network_unmanaged = add_error "NETWORK_UNMANAGED" -let network_incompatible_purposes = "NETWORK_INCOMPATIBLE_PURPOSES" +let network_incompatible_purposes = add_error "NETWORK_INCOMPATIBLE_PURPOSES" -let cannot_destroy_system_network = "CANNOT_DESTROY_SYSTEM_NETWORK" +let cannot_destroy_system_network = add_error "CANNOT_DESTROY_SYSTEM_NETWORK" -let pif_is_physical = "PIF_IS_PHYSICAL" +let pif_is_physical = add_error "PIF_IS_PHYSICAL" -let pif_is_not_physical = "PIF_IS_NOT_PHYSICAL" +let pif_is_not_physical = add_error "PIF_IS_NOT_PHYSICAL" -let pif_is_vlan = "PIF_IS_VLAN" +let pif_is_vlan = add_error "PIF_IS_VLAN" -let pif_is_sriov_logical = "PIF_IS_SRIOV_LOGICAL" +let pif_is_sriov_logical = add_error "PIF_IS_SRIOV_LOGICAL" -let pif_vlan_exists = "PIF_VLAN_EXISTS" +let pif_vlan_exists = add_error "PIF_VLAN_EXISTS" -let pif_vlan_still_exists = "PIF_VLAN_STILL_EXISTS" +let pif_vlan_still_exists = add_error "PIF_VLAN_STILL_EXISTS" -let vlan_in_use = "VLAN_IN_USE" +let vlan_in_use = add_error "VLAN_IN_USE" -let pif_device_not_found = "PIF_DEVICE_NOT_FOUND" +let pif_device_not_found = add_error "PIF_DEVICE_NOT_FOUND" -let pif_already_bonded = "PIF_ALREADY_BONDED" +let pif_already_bonded = add_error "PIF_ALREADY_BONDED" -let pif_cannot_bond_cross_host = "PIF_CANNOT_BOND_CROSS_HOST" +let pif_cannot_bond_cross_host = add_error "PIF_CANNOT_BOND_CROSS_HOST" -let pif_bond_needs_more_members = "PIF_BOND_NEEDS_MORE_MEMBERS" +let pif_bond_needs_more_members = add_error "PIF_BOND_NEEDS_MORE_MEMBERS" -let pif_bond_more_than_one_ip = "PIF_BOND_MORE_THAN_ONE_IP" +let pif_bond_more_than_one_ip = add_error "PIF_BOND_MORE_THAN_ONE_IP" -let pif_configuration_error = "PIF_CONFIGURATION_ERROR" +let pif_configuration_error = add_error "PIF_CONFIGURATION_ERROR" -let pif_is_management_iface = "PIF_IS_MANAGEMENT_INTERFACE" +let pif_is_management_iface = add_error "PIF_IS_MANAGEMENT_INTERFACE" let pif_incompatible_primary_address_type = - "PIF_INCOMPATIBLE_PRIMARY_ADDRESS_TYPE" + add_error "PIF_INCOMPATIBLE_PRIMARY_ADDRESS_TYPE" -let required_pif_is_unplugged = "REQUIRED_PIF_IS_UNPLUGGED" +let required_pif_is_unplugged = add_error "REQUIRED_PIF_IS_UNPLUGGED" -let pif_not_present = "PIF_NOT_PRESENT" +let pif_not_present = add_error "PIF_NOT_PRESENT" -let pif_does_not_allow_unplug = "PIF_DOES_NOT_ALLOW_UNPLUG" +let pif_does_not_allow_unplug = add_error "PIF_DOES_NOT_ALLOW_UNPLUG" -let pif_allows_unplug = "PIF_ALLOWS_UNPLUG" +let pif_allows_unplug = add_error "PIF_ALLOWS_UNPLUG" -let pif_has_fcoe_sr_in_use = "PIF_HAS_FCOE_SR_IN_USE" +let pif_has_fcoe_sr_in_use = add_error "PIF_HAS_FCOE_SR_IN_USE" -let pif_unmanaged = "PIF_UNMANAGED" +let pif_unmanaged = add_error "PIF_UNMANAGED" -let pif_is_not_sriov_capable = "PIF_IS_NOT_SRIOV_CAPABLE" +let pif_is_not_sriov_capable = add_error "PIF_IS_NOT_SRIOV_CAPABLE" -let pif_sriov_still_exists = "PIF_SRIOV_STILL_EXISTS" +let pif_sriov_still_exists = add_error "PIF_SRIOV_STILL_EXISTS" -let cannot_plug_bond_slave = "CANNOT_PLUG_BOND_SLAVE" +let cannot_plug_bond_slave = add_error "CANNOT_PLUG_BOND_SLAVE" -let cannot_add_vlan_to_bond_slave = "CANNOT_ADD_VLAN_TO_BOND_SLAVE" +let cannot_add_vlan_to_bond_slave = add_error "CANNOT_ADD_VLAN_TO_BOND_SLAVE" -let cannot_add_tunnel_to_bond_slave = "CANNOT_ADD_TUNNEL_TO_BOND_SLAVE" +let cannot_add_tunnel_to_bond_slave = + add_error "CANNOT_ADD_TUNNEL_TO_BOND_SLAVE" -let cannot_add_tunnel_to_sriov_logical = "CANNOT_ADD_TUNNEL_TO_SRIOV_LOGICAL" +let cannot_add_tunnel_to_sriov_logical = + add_error "CANNOT_ADD_TUNNEL_TO_SRIOV_LOGICAL" let cannot_add_tunnel_to_vlan_on_sriov_logical = - "CANNOT_ADD_TUNNEL_TO_VLAN_ON_SRIOV_LOGICAL" + add_error "CANNOT_ADD_TUNNEL_TO_VLAN_ON_SRIOV_LOGICAL" -let cannot_change_pif_properties = "CANNOT_CHANGE_PIF_PROPERTIES" +let cannot_change_pif_properties = add_error "CANNOT_CHANGE_PIF_PROPERTIES" -let cannot_forget_sriov_logical = "CANNOT_FORGET_SRIOV_LOGICAL" +let cannot_forget_sriov_logical = add_error "CANNOT_FORGET_SRIOV_LOGICAL" -let incompatible_pif_properties = "INCOMPATIBLE_PIF_PROPERTIES" +let incompatible_pif_properties = add_error "INCOMPATIBLE_PIF_PROPERTIES" -let slave_requires_management_iface = "SLAVE_REQUIRES_MANAGEMENT_INTERFACE" +let slave_requires_management_iface = + add_error "SLAVE_REQUIRES_MANAGEMENT_INTERFACE" -let vif_in_use = "VIF_IN_USE" +let vif_in_use = add_error "VIF_IN_USE" -let cannot_plug_vif = "CANNOT_PLUG_VIF" +let cannot_plug_vif = add_error "CANNOT_PLUG_VIF" -let mac_still_exists = "MAC_STILL_EXISTS" +let mac_still_exists = add_error "MAC_STILL_EXISTS" -let mac_does_not_exist = "MAC_DOES_NOT_EXIST" +let mac_does_not_exist = add_error "MAC_DOES_NOT_EXIST" -let mac_invalid = "MAC_INVALID" +let mac_invalid = add_error "MAC_INVALID" -let duplicate_pif_device_name = "DUPLICATE_PIF_DEVICE_NAME" +let duplicate_pif_device_name = add_error "DUPLICATE_PIF_DEVICE_NAME" let could_not_find_network_interface_with_specified_device_name_and_mac_address = - "COULD_NOT_FIND_NETWORK_INTERFACE_WITH_SPECIFIED_DEVICE_NAME_AND_MAC_ADDRESS" + add_error + "COULD_NOT_FIND_NETWORK_INTERFACE_WITH_SPECIFIED_DEVICE_NAME_AND_MAC_ADDRESS" -let openvswitch_not_active = "OPENVSWITCH_NOT_ACTIVE" +let openvswitch_not_active = add_error "OPENVSWITCH_NOT_ACTIVE" -let transport_pif_not_configured = "TRANSPORT_PIF_NOT_CONFIGURED" +let transport_pif_not_configured = add_error "TRANSPORT_PIF_NOT_CONFIGURED" -let is_tunnel_access_pif = "IS_TUNNEL_ACCESS_PIF" +let is_tunnel_access_pif = add_error "IS_TUNNEL_ACCESS_PIF" -let pif_tunnel_still_exists = "PIF_TUNNEL_STILL_EXISTS" +let pif_tunnel_still_exists = add_error "PIF_TUNNEL_STILL_EXISTS" -let bridge_not_available = "BRIDGE_NOT_AVAILABLE" +let bridge_not_available = add_error "BRIDGE_NOT_AVAILABLE" -let bridge_name_exists = "BRIDGE_NAME_EXISTS" +let bridge_name_exists = add_error "BRIDGE_NAME_EXISTS" -let vlan_tag_invalid = "VLAN_TAG_INVALID" +let vlan_tag_invalid = add_error "VLAN_TAG_INVALID" -let vm_bad_power_state = "VM_BAD_POWER_STATE" +let vm_bad_power_state = add_error "VM_BAD_POWER_STATE" -let vm_is_template = "VM_IS_TEMPLATE" +let vm_is_template = add_error "VM_IS_TEMPLATE" -let vm_is_snapshot = "VM_IS_SNAPSHOT" +let vm_is_snapshot = add_error "VM_IS_SNAPSHOT" -let other_operation_in_progress = "OTHER_OPERATION_IN_PROGRESS" +let other_operation_in_progress = add_error "OTHER_OPERATION_IN_PROGRESS" -let vbd_not_removable_media = "VBD_NOT_REMOVABLE_MEDIA" +let vbd_not_removable_media = add_error "VBD_NOT_REMOVABLE_MEDIA" -let vbd_not_unpluggable = "VBD_NOT_UNPLUGGABLE" +let vbd_not_unpluggable = add_error "VBD_NOT_UNPLUGGABLE" -let vbd_not_empty = "VBD_NOT_EMPTY" +let vbd_not_empty = add_error "VBD_NOT_EMPTY" -let vbd_is_empty = "VBD_IS_EMPTY" +let vbd_is_empty = add_error "VBD_IS_EMPTY" -let vbd_tray_locked = "VBD_TRAY_LOCKED" +let vbd_tray_locked = add_error "VBD_TRAY_LOCKED" -let vbd_missing = "VBD_MISSING" +let vbd_missing = add_error "VBD_MISSING" -let vm_no_empty_cd_vbd = "VM_NO_EMPTY_CD_VBD" +let vm_no_empty_cd_vbd = add_error "VM_NO_EMPTY_CD_VBD" -let vm_snapshot_failed = "VM_SNAPSHOT_FAILED" +let vm_snapshot_failed = add_error "VM_SNAPSHOT_FAILED" -let vm_snapshot_with_quiesce_failed = "VM_SNAPSHOT_WITH_QUIESCE_FAILED" +let vm_snapshot_with_quiesce_failed = + add_error "VM_SNAPSHOT_WITH_QUIESCE_FAILED" -let vm_snapshot_with_quiesce_timeout = "VM_SNAPSHOT_WITH_QUIESCE_TIMEOUT" +let vm_snapshot_with_quiesce_timeout = + add_error "VM_SNAPSHOT_WITH_QUIESCE_TIMEOUT" let vm_snapshot_with_quiesce_plugin_does_not_respond = - "VM_SNAPSHOT_WITH_QUIESCE_PLUGIN_DEOS_NOT_RESPOND" + add_error "VM_SNAPSHOT_WITH_QUIESCE_PLUGIN_DEOS_NOT_RESPOND" let vm_snapshot_with_quiesce_not_supported = - "VM_SNAPSHOT_WITH_QUIESCE_NOT_SUPPORTED" + add_error "VM_SNAPSHOT_WITH_QUIESCE_NOT_SUPPORTED" -let xen_vss_req_error_init_failed = "XEN_VSS_REQ_ERROR_INIT_FAILED" +let xen_vss_req_error_init_failed = add_error "XEN_VSS_REQ_ERROR_INIT_FAILED" -let xen_vss_req_error_prov_not_loaded = "XEN_VSS_REQ_ERROR_PROV_NOT_LOADED" +let xen_vss_req_error_prov_not_loaded = + add_error "XEN_VSS_REQ_ERROR_PROV_NOT_LOADED" let xen_vss_req_error_no_volumes_supported = - "XEN_VSS_REQ_ERROR_NO_VOLUMES_SUPPORTED" + add_error "XEN_VSS_REQ_ERROR_NO_VOLUMES_SUPPORTED" let xen_vss_req_error_start_snapshot_set_failed = - "XEN_VSS_REQ_ERROR_START_SNAPSHOT_SET_FAILED" + add_error "XEN_VSS_REQ_ERROR_START_SNAPSHOT_SET_FAILED" let xen_vss_req_error_adding_volume_to_snapset_failed = - "XEN_VSS_REQ_ERROR_ADDING_VOLUME_TO_SNAPSET_FAILED" + add_error "XEN_VSS_REQ_ERROR_ADDING_VOLUME_TO_SNAPSET_FAILED" -let xen_vss_req_error_preparing_writers = "XEN_VSS_REQ_ERROR_PREPARING_WRITERS" +let xen_vss_req_error_preparing_writers = + add_error "XEN_VSS_REQ_ERROR_PREPARING_WRITERS" -let xen_vss_req_error_creating_snapshot = "XEN_VSS_REQ_ERROR_CREATING_SNAPSHOT" +let xen_vss_req_error_creating_snapshot = + add_error "XEN_VSS_REQ_ERROR_CREATING_SNAPSHOT" let xen_vss_req_error_creating_snapshot_xml_string = - "XEN_VSS_REQ_ERROR_CREATING_SNAPSHOT_XML_STRING" + add_error "XEN_VSS_REQ_ERROR_CREATING_SNAPSHOT_XML_STRING" -let vm_revert_failed = "VM_REVERT_FAILED" +let vm_revert_failed = add_error "VM_REVERT_FAILED" -let vm_checkpoint_suspend_failed = "VM_CHECKPOINT_SUSPEND_FAILED" +let vm_checkpoint_suspend_failed = add_error "VM_CHECKPOINT_SUSPEND_FAILED" -let vm_checkpoint_resume_failed = "VM_CHECKPOINT_RESUME_FAILED" +let vm_checkpoint_resume_failed = add_error "VM_CHECKPOINT_RESUME_FAILED" -let vm_unsafe_boot = "VM_UNSAFE_BOOT" +let vm_unsafe_boot = add_error "VM_UNSAFE_BOOT" -let vm_requires_sr = "VM_REQUIRES_SR" +let vm_requires_sr = add_error "VM_REQUIRES_SR" -let vm_requires_vdi = "VM_REQUIRES_VDI" +let vm_requires_vdi = add_error "VM_REQUIRES_VDI" -let vm_requires_net = "VM_REQUIRES_NETWORK" +let vm_requires_net = add_error "VM_REQUIRES_NETWORK" -let vm_requires_gpu = "VM_REQUIRES_GPU" +let vm_requires_gpu = add_error "VM_REQUIRES_GPU" -let vm_requires_vgpu = "VM_REQUIRES_VGPU" +let vm_requires_vgpu = add_error "VM_REQUIRES_VGPU" -let vm_requires_iommu = "VM_REQUIRES_IOMMU" +let vm_requires_iommu = add_error "VM_REQUIRES_IOMMU" let vm_host_incompatible_version_migrate = - "VM_HOST_INCOMPATIBLE_VERSION_MIGRATE" + add_error "VM_HOST_INCOMPATIBLE_VERSION_MIGRATE" -let vm_host_incompatible_version = "VM_HOST_INCOMPATIBLE_VERSION" +let vm_host_incompatible_version = add_error "VM_HOST_INCOMPATIBLE_VERSION" let vm_host_incompatible_virtual_hardware_platform_version = - "VM_HOST_INCOMPATIBLE_VIRTUAL_HARDWARE_PLATFORM_VERSION" + add_error "VM_HOST_INCOMPATIBLE_VIRTUAL_HARDWARE_PLATFORM_VERSION" -let vm_has_pci_attached = "VM_HAS_PCI_ATTACHED" +let vm_has_pci_attached = add_error "VM_HAS_PCI_ATTACHED" -let vm_has_vgpu = "VM_HAS_VGPU" +let vm_has_vgpu = add_error "VM_HAS_VGPU" -let vm_has_sriov_vif = "VM_HAS_SRIOV_VIF" +let vm_has_sriov_vif = add_error "VM_HAS_SRIOV_VIF" -let vm_has_no_suspend_vdi = "VM_HAS_NO_SUSPEND_VDI" +let vm_has_no_suspend_vdi = add_error "VM_HAS_NO_SUSPEND_VDI" -let host_cannot_attach_network = "HOST_CANNOT_ATTACH_NETWORK" +let host_cannot_attach_network = add_error "HOST_CANNOT_ATTACH_NETWORK" -let vm_no_suspend_sr = "VM_NO_SUSPEND_SR" +let vm_no_suspend_sr = add_error "VM_NO_SUSPEND_SR" -let vm_no_crashdump_sr = "VM_NO_CRASHDUMP_SR" +let vm_no_crashdump_sr = add_error "VM_NO_CRASHDUMP_SR" -let vm_migrate_failed = "VM_MIGRATE_FAILED" +let vm_migrate_failed = add_error "VM_MIGRATE_FAILED" let vm_migrate_contact_remote_service_failed = - "VM_MIGRATE_CONTACT_REMOTE_SERVICE_FAILED" + add_error "VM_MIGRATE_CONTACT_REMOTE_SERVICE_FAILED" -let vm_missing_pv_drivers = "VM_MISSING_PV_DRIVERS" +let vm_missing_pv_drivers = add_error "VM_MISSING_PV_DRIVERS" -let vm_failed_shutdown_ack = "VM_FAILED_SHUTDOWN_ACKNOWLEDGMENT" +let vm_failed_shutdown_ack = add_error "VM_FAILED_SHUTDOWN_ACKNOWLEDGMENT" -let vm_failed_suspend_ack = "VM_FAILED_SUSPEND_ACKNOWLEDGMENT" +let vm_failed_suspend_ack = add_error "VM_FAILED_SUSPEND_ACKNOWLEDGMENT" -let vm_old_pv_drivers = "VM_OLD_PV_DRIVERS" +let vm_old_pv_drivers = add_error "VM_OLD_PV_DRIVERS" -let vm_lacks_feature = "VM_LACKS_FEATURE" +let vm_lacks_feature = add_error "VM_LACKS_FEATURE" -let vm_lacks_feature_shutdown = "VM_LACKS_FEATURE_SHUTDOWN" +let vm_lacks_feature_shutdown = add_error "VM_LACKS_FEATURE_SHUTDOWN" -let vm_lacks_feature_suspend = "VM_LACKS_FEATURE_SUSPEND" +let vm_lacks_feature_suspend = add_error "VM_LACKS_FEATURE_SUSPEND" -let vm_lacks_feature_vcpu_hotplug = "VM_LACKS_FEATURE_VCPU_HOTPLUG" +let vm_lacks_feature_vcpu_hotplug = add_error "VM_LACKS_FEATURE_VCPU_HOTPLUG" -let vm_lacks_feature_static_ip_setting = "VM_LACKS_FEATURE_STATIC_IP_SETTING" +let vm_lacks_feature_static_ip_setting = + add_error "VM_LACKS_FEATURE_STATIC_IP_SETTING" -let vm_cannot_delete_default_template = "VM_CANNOT_DELETE_DEFAULT_TEMPLATE" +let vm_cannot_delete_default_template = + add_error "VM_CANNOT_DELETE_DEFAULT_TEMPLATE" -let vm_memory_size_too_low = "VM_MEMORY_SIZE_TOO_LOW" +let vm_memory_size_too_low = add_error "VM_MEMORY_SIZE_TOO_LOW" -let vm_memory_target_wait_timeout = "VM_MEMORY_TARGET_WAIT_TIMEOUT" +let vm_memory_target_wait_timeout = add_error "VM_MEMORY_TARGET_WAIT_TIMEOUT" -let vm_shutdown_timeout = "VM_SHUTDOWN_TIMEOUT" +let vm_shutdown_timeout = add_error "VM_SHUTDOWN_TIMEOUT" -let vm_suspend_timeout = "VM_SUSPEND_TIMEOUT" +let vm_suspend_timeout = add_error "VM_SUSPEND_TIMEOUT" -let vm_duplicate_vbd_device = "VM_DUPLICATE_VBD_DEVICE" +let vm_duplicate_vbd_device = add_error "VM_DUPLICATE_VBD_DEVICE" -let illegal_vbd_device = "ILLEGAL_VBD_DEVICE" +let illegal_vbd_device = add_error "ILLEGAL_VBD_DEVICE" -let vm_not_resident_here = "VM_NOT_RESIDENT_HERE" +let vm_not_resident_here = add_error "VM_NOT_RESIDENT_HERE" -let vm_crashed = "VM_CRASHED" +let vm_crashed = add_error "VM_CRASHED" -let vm_rebooted = "VM_REBOOTED" +let vm_rebooted = add_error "VM_REBOOTED" -let vm_halted = "VM_HALTED" +let vm_halted = add_error "VM_HALTED" let vm_attached_to_more_than_one_vdi_with_timeoffset_marked_as_reset_on_boot = - "VM_ATTACHED_TO_MORE_THAN_ONE_VDI_WITH_TIMEOFFSET_MARKED_AS_RESET_ON_BOOT" + add_error + "VM_ATTACHED_TO_MORE_THAN_ONE_VDI_WITH_TIMEOFFSET_MARKED_AS_RESET_ON_BOOT" -let vms_failed_to_cooperate = "VMS_FAILED_TO_COOPERATE" +let vms_failed_to_cooperate = add_error "VMS_FAILED_TO_COOPERATE" -let vm_pv_drivers_in_use = "VM_PV_DRIVERS_IN_USE" +let vm_pv_drivers_in_use = add_error "VM_PV_DRIVERS_IN_USE" -let domain_exists = "DOMAIN_EXISTS" +let domain_exists = add_error "DOMAIN_EXISTS" -let cannot_reset_control_domain = "CANNOT_RESET_CONTROL_DOMAIN" +let cannot_reset_control_domain = add_error "CANNOT_RESET_CONTROL_DOMAIN" -let not_system_domain = "NOT_SYSTEM_DOMAIN" +let not_system_domain = add_error "NOT_SYSTEM_DOMAIN" -let only_provision_template = "PROVISION_ONLY_ALLOWED_ON_TEMPLATE" +let only_provision_template = add_error "PROVISION_ONLY_ALLOWED_ON_TEMPLATE" -let only_revert_snapshot = "REVERT_ONLY_ALLOWED_ON_SNAPSHOT" +let only_revert_snapshot = add_error "REVERT_ONLY_ALLOWED_ON_SNAPSHOT" -let provision_failed_out_of_space = "PROVISION_FAILED_OUT_OF_SPACE" +let provision_failed_out_of_space = add_error "PROVISION_FAILED_OUT_OF_SPACE" -let bootloader_failed = "BOOTLOADER_FAILED" +let bootloader_failed = add_error "BOOTLOADER_FAILED" -let unknown_bootloader = "UNKNOWN_BOOTLOADER" +let unknown_bootloader = add_error "UNKNOWN_BOOTLOADER" -let failed_to_start_emulator = "FAILED_TO_START_EMULATOR" +let failed_to_start_emulator = add_error "FAILED_TO_START_EMULATOR" -let object_nolonger_exists = "OBJECT_NOLONGER_EXISTS" +let object_nolonger_exists = add_error "OBJECT_NOLONGER_EXISTS" -let sr_attach_failed = "SR_ATTACH_FAILED" +let sr_attach_failed = add_error "SR_ATTACH_FAILED" -let sr_full = "SR_FULL" +let sr_full = add_error "SR_FULL" -let sr_source_space_insufficient = "SR_SOURCE_SPACE_INSUFFICIENT" +let sr_source_space_insufficient = add_error "SR_SOURCE_SPACE_INSUFFICIENT" -let sr_has_pbd = "SR_HAS_PBD" +let sr_has_pbd = add_error "SR_HAS_PBD" -let sr_requires_upgrade = "SR_REQUIRES_UPGRADE" +let sr_requires_upgrade = add_error "SR_REQUIRES_UPGRADE" -let sr_is_cache_sr = "SR_IS_CACHE_SR" +let sr_is_cache_sr = add_error "SR_IS_CACHE_SR" -let vdi_in_use = "VDI_IN_USE" +let vdi_in_use = add_error "VDI_IN_USE" -let vdi_is_sharable = "VDI_IS_SHARABLE" +let vdi_is_sharable = add_error "VDI_IS_SHARABLE" -let vdi_readonly = "VDI_READONLY" +let vdi_readonly = add_error "VDI_READONLY" -let vdi_too_small = "VDI_TOO_SMALL" +let vdi_too_small = add_error "VDI_TOO_SMALL" -let vdi_too_large = "VDI_TOO_LARGE" +let vdi_too_large = add_error "VDI_TOO_LARGE" -let vdi_not_sparse = "VDI_NOT_SPARSE" +let vdi_not_sparse = add_error "VDI_NOT_SPARSE" -let vdi_is_a_physical_device = "VDI_IS_A_PHYSICAL_DEVICE" +let vdi_is_a_physical_device = add_error "VDI_IS_A_PHYSICAL_DEVICE" -let vdi_is_not_iso = "VDI_IS_NOT_ISO" +let vdi_is_not_iso = add_error "VDI_IS_NOT_ISO" -let vbd_cds_must_be_readonly = "VBD_CDS_MUST_BE_READONLY" +let vbd_cds_must_be_readonly = add_error "VBD_CDS_MUST_BE_READONLY" -let vm_requires_vusb = "VM_REQUIRES_VUSB" +let vm_requires_vusb = add_error "VM_REQUIRES_VUSB" (* CA-83260 *) -let disk_vbd_must_be_readwrite_for_hvm = "DISK_VBD_MUST_BE_READWRITE_FOR_HVM" +let disk_vbd_must_be_readwrite_for_hvm = + add_error "DISK_VBD_MUST_BE_READWRITE_FOR_HVM" -let host_cd_drive_empty = "HOST_CD_DRIVE_EMPTY" +let host_cd_drive_empty = add_error "HOST_CD_DRIVE_EMPTY" -let vdi_not_available = "VDI_NOT_AVAILABLE" +let vdi_not_available = add_error "VDI_NOT_AVAILABLE" -let vdi_has_rrds = "VDI_HAS_RRDS" +let vdi_has_rrds = add_error "VDI_HAS_RRDS" -let vdi_location_missing = "VDI_LOCATION_MISSING" +let vdi_location_missing = add_error "VDI_LOCATION_MISSING" -let vdi_content_id_missing = "VDI_CONTENT_ID_MISSING" +let vdi_content_id_missing = add_error "VDI_CONTENT_ID_MISSING" -let vdi_missing = "VDI_MISSING" +let vdi_missing = add_error "VDI_MISSING" -let vdi_incompatible_type = "VDI_INCOMPATIBLE_TYPE" +let vdi_incompatible_type = add_error "VDI_INCOMPATIBLE_TYPE" -let vdi_not_managed = "VDI_NOT_MANAGED" +let vdi_not_managed = add_error "VDI_NOT_MANAGED" -let vdi_io_error = "VDI_IO_ERROR" +let vdi_io_error = add_error "VDI_IO_ERROR" let vdi_on_boot_mode_incompatible_with_operation = - "VDI_ON_BOOT_MODE_INCOMPATIBLE_WITH_OPERATION" + add_error "VDI_ON_BOOT_MODE_INCOMPATIBLE_WITH_OPERATION" -let vdi_not_in_map = "VDI_NOT_IN_MAP" +let vdi_not_in_map = add_error "VDI_NOT_IN_MAP" -let vdi_cbt_enabled = "VDI_CBT_ENABLED" +let vdi_cbt_enabled = add_error "VDI_CBT_ENABLED" -let vdi_no_cbt_metadata = "VDI_NO_CBT_METADATA" +let vdi_no_cbt_metadata = add_error "VDI_NO_CBT_METADATA" -let vdi_is_encrypted = "VDI_IS_ENCRYPTED" +let vdi_is_encrypted = add_error "VDI_IS_ENCRYPTED" -let vif_not_in_map = "VIF_NOT_IN_MAP" +let vif_not_in_map = add_error "VIF_NOT_IN_MAP" -let cannot_create_state_file = "CANNOT_CREATE_STATE_FILE" +let cannot_create_state_file = add_error "CANNOT_CREATE_STATE_FILE" -let operation_partially_failed = "OPERATION_PARTIALLY_FAILED" +let operation_partially_failed = add_error "OPERATION_PARTIALLY_FAILED" -let sr_uuid_exists = "SR_UUID_EXISTS" +let sr_uuid_exists = add_error "SR_UUID_EXISTS" -let sr_no_pbds = "SR_HAS_NO_PBDS" +let sr_no_pbds = add_error "SR_HAS_NO_PBDS" -let sr_has_multiple_pbds = "SR_HAS_MULTIPLE_PBDS" +let sr_has_multiple_pbds = add_error "SR_HAS_MULTIPLE_PBDS" -let sr_backend_failure = "SR_BACKEND_FAILURE" +let sr_backend_failure = add_error "SR_BACKEND_FAILURE" -let sr_unknown_driver = "SR_UNKNOWN_DRIVER" +let sr_unknown_driver = add_error "SR_UNKNOWN_DRIVER" -let sr_vdi_locking_failed = "SR_VDI_LOCKING_FAILED" +let sr_vdi_locking_failed = add_error "SR_VDI_LOCKING_FAILED" -let sr_not_empty = "SR_NOT_EMPTY" +let sr_not_empty = add_error "SR_NOT_EMPTY" -let sr_device_in_use = "SR_DEVICE_IN_USE" +let sr_device_in_use = add_error "SR_DEVICE_IN_USE" -let sr_operation_not_supported = "SR_OPERATION_NOT_SUPPORTED" +let sr_operation_not_supported = add_error "SR_OPERATION_NOT_SUPPORTED" -let sr_not_sharable = "SR_NOT_SHARABLE" +let sr_not_sharable = add_error "SR_NOT_SHARABLE" -let sr_indestructible = "SR_INDESTRUCTIBLE" +let sr_indestructible = add_error "SR_INDESTRUCTIBLE" -let clustered_sr_degraded = "CLUSTERED_SR_DEGRADED" +let clustered_sr_degraded = add_error "CLUSTERED_SR_DEGRADED" -let sm_plugin_communication_failure = "SM_PLUGIN_COMMUNICATION_FAILURE" +let sm_plugin_communication_failure = + add_error "SM_PLUGIN_COMMUNICATION_FAILURE" -let pbd_exists = "PBD_EXISTS" +let pbd_exists = add_error "PBD_EXISTS" -let not_implemented = "NOT_IMPLEMENTED" +let not_implemented = add_error "NOT_IMPLEMENTED" -let device_already_attached = "DEVICE_ALREADY_ATTACHED" +let device_already_attached = add_error "DEVICE_ALREADY_ATTACHED" -let device_already_detached = "DEVICE_ALREADY_DETACHED" +let device_already_detached = add_error "DEVICE_ALREADY_DETACHED" -let device_already_exists = "DEVICE_ALREADY_EXISTS" +let device_already_exists = add_error "DEVICE_ALREADY_EXISTS" -let device_not_attached = "DEVICE_NOT_ATTACHED" +let device_not_attached = add_error "DEVICE_NOT_ATTACHED" -let network_contains_pif = "NETWORK_CONTAINS_PIF" +let network_contains_pif = add_error "NETWORK_CONTAINS_PIF" -let network_contains_vif = "NETWORK_CONTAINS_VIF" +let network_contains_vif = add_error "NETWORK_CONTAINS_VIF" -let gpu_group_contains_vgpu = "GPU_GROUP_CONTAINS_VGPU" +let gpu_group_contains_vgpu = add_error "GPU_GROUP_CONTAINS_VGPU" -let gpu_group_contains_pgpu = "GPU_GROUP_CONTAINS_PGPU" +let gpu_group_contains_pgpu = add_error "GPU_GROUP_CONTAINS_PGPU" -let gpu_group_contains_no_pgpus = "GPU_GROUP_CONTAINS_NO_PGPUS" +let gpu_group_contains_no_pgpus = add_error "GPU_GROUP_CONTAINS_NO_PGPUS" -let invalid_device = "INVALID_DEVICE" +let invalid_device = add_error "INVALID_DEVICE" -let events_lost = "EVENTS_LOST" +let events_lost = add_error "EVENTS_LOST" -let event_subscription_parse_failure = "EVENT_SUBSCRIPTION_PARSE_FAILURE" +let event_subscription_parse_failure = + add_error "EVENT_SUBSCRIPTION_PARSE_FAILURE" -let event_from_token_parse_failure = "EVENT_FROM_TOKEN_PARSE_FAILURE" +let event_from_token_parse_failure = add_error "EVENT_FROM_TOKEN_PARSE_FAILURE" -let session_not_registered = "SESSION_NOT_REGISTERED" +let session_not_registered = add_error "SESSION_NOT_REGISTERED" -let pgpu_in_use_by_vm = "PGPU_IN_USE_BY_VM" +let pgpu_in_use_by_vm = add_error "PGPU_IN_USE_BY_VM" -let pgpu_not_compatible_with_gpu_group = "PGPU_NOT_COMPATIBLE_WITH_GPU_GROUP" +let pgpu_not_compatible_with_gpu_group = + add_error "PGPU_NOT_COMPATIBLE_WITH_GPU_GROUP" -let pgpu_insufficient_capacity_for_vgpu = "PGPU_INSUFFICIENT_CAPACITY_FOR_VGPU" +let pgpu_insufficient_capacity_for_vgpu = + add_error "PGPU_INSUFFICIENT_CAPACITY_FOR_VGPU" -let vgpu_type_not_enabled = "VGPU_TYPE_NOT_ENABLED" +let vgpu_type_not_enabled = add_error "VGPU_TYPE_NOT_ENABLED" -let vgpu_type_not_supported = "VGPU_TYPE_NOT_SUPPORTED" +let vgpu_type_not_supported = add_error "VGPU_TYPE_NOT_SUPPORTED" -let vgpu_type_no_longer_supported = "VGPU_TYPE_NO_LONGER_SUPPORTED" +let vgpu_type_no_longer_supported = add_error "VGPU_TYPE_NO_LONGER_SUPPORTED" let vgpu_type_not_compatible_with_running_type = - "VGPU_TYPE_NOT_COMPATIBLE_WITH_RUNNING_TYPE" + add_error "VGPU_TYPE_NOT_COMPATIBLE_WITH_RUNNING_TYPE" -let vgpu_type_not_compatible = "VGPU_TYPE_NOT_COMPATIBLE" +let vgpu_type_not_compatible = add_error "VGPU_TYPE_NOT_COMPATIBLE" -let vgpu_destination_incompatible = "VGPU_DESTINATION_INCOMPATIBLE" +let vgpu_destination_incompatible = add_error "VGPU_DESTINATION_INCOMPATIBLE" -let vgpu_suspension_not_supported = "VGPU_SUSPENSION_NOT_SUPPORTED" +let vgpu_suspension_not_supported = add_error "VGPU_SUSPENSION_NOT_SUPPORTED" -let vgpu_guest_driver_limit = "VGPU_GUEST_DRIVER_LIMIT" +let vgpu_guest_driver_limit = add_error "VGPU_GUEST_DRIVER_LIMIT" -let nvidia_tools_error = "NVIDIA_TOOLS_ERROR" +let nvidia_tools_error = add_error "NVIDIA_TOOLS_ERROR" -let nvidia_sriov_misconfigured = "NVIDIA_SRIOV_MISCONFIGURED" +let nvidia_sriov_misconfigured = add_error "NVIDIA_SRIOV_MISCONFIGURED" -let vm_pci_bus_full = "VM_PCI_BUS_FULL" +let vm_pci_bus_full = add_error "VM_PCI_BUS_FULL" -let import_error_generic = "IMPORT_ERROR" +let import_error_generic = add_error "IMPORT_ERROR" -let import_error_premature_eof = "IMPORT_ERROR_PREMATURE_EOF" +let import_error_premature_eof = add_error "IMPORT_ERROR_PREMATURE_EOF" -let import_error_some_checksums_failed = "IMPORT_ERROR_SOME_CHECKSUMS_FAILED" +let import_error_some_checksums_failed = + add_error "IMPORT_ERROR_SOME_CHECKSUMS_FAILED" -let import_error_cannot_handle_chunked = "IMPORT_ERROR_CANNOT_HANDLE_CHUNKED" +let import_error_cannot_handle_chunked = + add_error "IMPORT_ERROR_CANNOT_HANDLE_CHUNKED" -let import_error_failed_to_find_object = "IMPORT_ERROR_FAILED_TO_FIND_OBJECT" +let import_error_failed_to_find_object = + add_error "IMPORT_ERROR_FAILED_TO_FIND_OBJECT" let import_error_attached_disks_not_found = - "IMPORT_ERROR_ATTACHED_DISKS_NOT_FOUND" + add_error "IMPORT_ERROR_ATTACHED_DISKS_NOT_FOUND" -let import_error_unexpected_file = "IMPORT_ERROR_UNEXPECTED_FILE" +let import_error_unexpected_file = add_error "IMPORT_ERROR_UNEXPECTED_FILE" -let import_incompatible_version = "IMPORT_INCOMPATIBLE_VERSION" +let import_incompatible_version = add_error "IMPORT_INCOMPATIBLE_VERSION" -let restore_incompatible_version = "RESTORE_INCOMPATIBLE_VERSION" +let restore_incompatible_version = add_error "RESTORE_INCOMPATIBLE_VERSION" -let restore_target_missing_device = "RESTORE_TARGET_MISSING_DEVICE" +let restore_target_missing_device = add_error "RESTORE_TARGET_MISSING_DEVICE" let restore_target_mgmt_if_not_in_backup = - "RESTORE_TARGET_MGMT_IF_NOT_IN_BACKUP" + add_error "RESTORE_TARGET_MGMT_IF_NOT_IN_BACKUP" -let pool_not_in_emergency_mode = "NOT_IN_EMERGENCY_MODE" +let pool_not_in_emergency_mode = add_error "NOT_IN_EMERGENCY_MODE" -let pool_hosts_not_compatible = "HOSTS_NOT_COMPATIBLE" +let pool_hosts_not_compatible = add_error "HOSTS_NOT_COMPATIBLE" -let pool_hosts_not_homogeneous = "HOSTS_NOT_HOMOGENEOUS" +let pool_hosts_not_homogeneous = add_error "HOSTS_NOT_HOMOGENEOUS" let pool_joining_host_cannot_contain_shared_SRs = - "JOINING_HOST_CANNOT_CONTAIN_SHARED_SRS" + add_error "JOINING_HOST_CANNOT_CONTAIN_SHARED_SRS" let pool_joining_host_cannot_have_running_or_suspended_VMs = - "JOINING_HOST_CANNOT_HAVE_RUNNING_OR_SUSPENDED_VMS" + add_error "JOINING_HOST_CANNOT_HAVE_RUNNING_OR_SUSPENDED_VMS" let pool_joining_host_cannot_have_running_VMs = - "JOINING_HOST_CANNOT_HAVE_RUNNING_VMS" + add_error "JOINING_HOST_CANNOT_HAVE_RUNNING_VMS" let pool_joining_host_cannot_have_vms_with_current_operations = - "JOINING_HOST_CANNOT_HAVE_VMS_WITH_CURRENT_OPERATIONS" + add_error "JOINING_HOST_CANNOT_HAVE_VMS_WITH_CURRENT_OPERATIONS" let pool_joining_host_cannot_be_master_of_other_hosts = - "JOINING_HOST_CANNOT_BE_MASTER_OF_OTHER_HOSTS" + add_error "JOINING_HOST_CANNOT_BE_MASTER_OF_OTHER_HOSTS" -let pool_joining_host_connection_failed = "JOINING_HOST_CONNECTION_FAILED" +let pool_joining_host_connection_failed = + add_error "JOINING_HOST_CONNECTION_FAILED" -let pool_joining_host_service_failed = "JOINING_HOST_SERVICE_FAILED" +let pool_joining_host_service_failed = add_error "JOINING_HOST_SERVICE_FAILED" let pool_joining_host_must_have_physical_management_nic = - "POOL_JOINING_HOST_MUST_HAVE_PHYSICAL_MANAGEMENT_NIC" + add_error "POOL_JOINING_HOST_MUST_HAVE_PHYSICAL_MANAGEMENT_NIC" -let pool_joining_external_auth_mismatch = "POOL_JOINING_EXTERNAL_AUTH_MISMATCH" +let pool_joining_external_auth_mismatch = + add_error "POOL_JOINING_EXTERNAL_AUTH_MISMATCH" let pool_joining_host_must_have_same_product_version = - "POOL_JOINING_HOST_MUST_HAVE_SAME_PRODUCT_VERSION" + add_error "POOL_JOINING_HOST_MUST_HAVE_SAME_PRODUCT_VERSION" let pool_joining_host_must_have_same_api_version = - "POOL_JOINING_HOST_MUST_HAVE_SAME_API_VERSION" + add_error "POOL_JOINING_HOST_MUST_HAVE_SAME_API_VERSION" let pool_joining_host_must_have_same_db_schema = - "POOL_JOINING_HOST_MUST_HAVE_SAME_DB_SCHEMA" + add_error "POOL_JOINING_HOST_MUST_HAVE_SAME_DB_SCHEMA" let pool_joining_host_must_only_have_physical_pifs = - "POOL_JOINING_HOST_MUST_ONLY_HAVE_PHYSICAL_PIFS" + add_error "POOL_JOINING_HOST_MUST_ONLY_HAVE_PHYSICAL_PIFS" let pool_joining_host_management_vlan_does_not_match = - "POOL_JOINING_HOST_MANAGEMENT_VLAN_DOES_NOT_MATCH" + add_error "POOL_JOINING_HOST_MANAGEMENT_VLAN_DOES_NOT_MATCH" let pool_joining_host_has_non_management_vlans = - "POOL_JOINING_HOST_HAS_NON_MANAGEMENT_VLANS" + add_error "POOL_JOINING_HOST_HAS_NON_MANAGEMENT_VLANS" -let pool_joining_host_has_bonds = "POOL_JOINING_HOST_HAS_BONDS" +let pool_joining_host_has_bonds = add_error "POOL_JOINING_HOST_HAS_BONDS" -let pool_joining_host_has_tunnels = "POOL_JOINING_HOST_HAS_TUNNELS" +let pool_joining_host_has_tunnels = add_error "POOL_JOINING_HOST_HAS_TUNNELS" let pool_joining_host_has_network_sriovs = - "POOL_JOINING_HOST_HAS_NETWORK_SRIOVS" + add_error "POOL_JOINING_HOST_HAS_NETWORK_SRIOVS" let pool_joining_host_tls_verification_mismatch = - "POOL_JOINING_HOST_TLS_VERIFICATION_MISMATCH" + add_error "POOL_JOINING_HOST_TLS_VERIFICATION_MISMATCH" let pool_joining_host_ca_certificates_conflict = - "POOL_JOINING_HOST_CA_CERTIFICATES_CONFLICT" + add_error "POOL_JOINING_HOST_CA_CERTIFICATES_CONFLICT" (*workload balancing*) -let wlb_not_initialized = "WLB_NOT_INITIALIZED" +let wlb_not_initialized = add_error "WLB_NOT_INITIALIZED" -let wlb_disabled = "WLB_DISABLED" +let wlb_disabled = add_error "WLB_DISABLED" -let wlb_connection_refused = "WLB_CONNECTION_REFUSED" +let wlb_connection_refused = add_error "WLB_CONNECTION_REFUSED" -let wlb_unknown_host = "WLB_UNKNOWN_HOST" +let wlb_unknown_host = add_error "WLB_UNKNOWN_HOST" -let wlb_timeout = "WLB_TIMEOUT" +let wlb_timeout = add_error "WLB_TIMEOUT" -let wlb_authentication_failed = "WLB_AUTHENTICATION_FAILED" +let wlb_authentication_failed = add_error "WLB_AUTHENTICATION_FAILED" -let wlb_malformed_request = "WLB_MALFORMED_REQUEST" +let wlb_malformed_request = add_error "WLB_MALFORMED_REQUEST" -let wlb_malformed_response = "WLB_MALFORMED_RESPONSE" +let wlb_malformed_response = add_error "WLB_MALFORMED_RESPONSE" -let wlb_xenserver_connection_refused = "WLB_XENSERVER_CONNECTION_REFUSED" +let wlb_xenserver_connection_refused = + add_error "WLB_XENSERVER_CONNECTION_REFUSED" -let wlb_xenserver_unknown_host = "WLB_XENSERVER_UNKNOWN_HOST" +let wlb_xenserver_unknown_host = add_error "WLB_XENSERVER_UNKNOWN_HOST" -let wlb_xenserver_timeout = "WLB_XENSERVER_TIMEOUT" +let wlb_xenserver_timeout = add_error "WLB_XENSERVER_TIMEOUT" -let wlb_xenserver_authentication_failed = "WLB_XENSERVER_AUTHENTICATION_FAILED" +let wlb_xenserver_authentication_failed = + add_error "WLB_XENSERVER_AUTHENTICATION_FAILED" -let wlb_xenserver_malformed_response = "WLB_XENSERVER_MALFORMED_RESPONSE" +let wlb_xenserver_malformed_response = + add_error "WLB_XENSERVER_MALFORMED_RESPONSE" -let wlb_internal_error = "WLB_INTERNAL_ERROR" +let wlb_internal_error = add_error "WLB_INTERNAL_ERROR" -let wlb_url_invalid = "WLB_URL_INVALID" +let wlb_url_invalid = add_error "WLB_URL_INVALID" -let wlb_connection_reset = "WLB_CONNECTION_RESET" +let wlb_connection_reset = add_error "WLB_CONNECTION_RESET" -let sr_not_shared = "SR_NOT_SHARED" +let sr_not_shared = add_error "SR_NOT_SHARED" -let default_sr_not_found = "DEFAULT_SR_NOT_FOUND" +let default_sr_not_found = add_error "DEFAULT_SR_NOT_FOUND" -let task_cancelled = "TASK_CANCELLED" +let task_cancelled = add_error "TASK_CANCELLED" -let too_many_pending_tasks = "TOO_MANY_PENDING_TASKS" +let too_many_pending_tasks = add_error "TOO_MANY_PENDING_TASKS" -let too_busy = "TOO_BUSY" +let too_busy = add_error "TOO_BUSY" -let out_of_space = "OUT_OF_SPACE" +let out_of_space = add_error "OUT_OF_SPACE" -let invalid_patch = "INVALID_PATCH" +let invalid_patch = add_error "INVALID_PATCH" -let invalid_update = "INVALID_UPDATE" +let invalid_update = add_error "INVALID_UPDATE" -let invalid_patch_with_log = "INVALID_PATCH_WITH_LOG" +let invalid_patch_with_log = add_error "INVALID_PATCH_WITH_LOG" -let patch_already_exists = "PATCH_ALREADY_EXISTS" +let patch_already_exists = add_error "PATCH_ALREADY_EXISTS" -let update_already_exists = "UPDATE_ALREADY_EXISTS" +let update_already_exists = add_error "UPDATE_ALREADY_EXISTS" -let patch_is_applied = "PATCH_IS_APPLIED" +let patch_is_applied = add_error "PATCH_IS_APPLIED" -let update_is_applied = "UPDATE_IS_APPLIED" +let update_is_applied = add_error "UPDATE_IS_APPLIED" -let cannot_find_patch = "CANNOT_FIND_PATCH" +let cannot_find_patch = add_error "CANNOT_FIND_PATCH" -let cannot_find_update = "CANNOT_FIND_UPDATE" +let cannot_find_update = add_error "CANNOT_FIND_UPDATE" -let cannot_fetch_patch = "CANNOT_FETCH_PATCH" +let cannot_fetch_patch = add_error "CANNOT_FETCH_PATCH" -let patch_already_applied = "PATCH_ALREADY_APPLIED" +let patch_already_applied = add_error "PATCH_ALREADY_APPLIED" -let update_already_applied = "UPDATE_ALREADY_APPLIED" +let update_already_applied = add_error "UPDATE_ALREADY_APPLIED" -let update_already_applied_in_pool = "UPDATE_ALREADY_APPLIED_IN_POOL" +let update_already_applied_in_pool = add_error "UPDATE_ALREADY_APPLIED_IN_POOL" -let update_pool_apply_failed = "UPDATE_POOL_APPLY_FAILED" +let update_pool_apply_failed = add_error "UPDATE_POOL_APPLY_FAILED" let could_not_update_igmp_snooping_everywhere = - "COULD_NOT_UPDATE_IGMP_SNOOPING_EVERYWHERE" + add_error "COULD_NOT_UPDATE_IGMP_SNOOPING_EVERYWHERE" -let update_apply_failed = "UPDATE_APPLY_FAILED" +let update_apply_failed = add_error "UPDATE_APPLY_FAILED" let update_precheck_failed_unknown_error = - "UPDATE_PRECHECK_FAILED_UNKNOWN_ERROR" + add_error "UPDATE_PRECHECK_FAILED_UNKNOWN_ERROR" let update_precheck_failed_prerequisite_missing = - "UPDATE_PRECHECK_FAILED_PREREQUISITE_MISSING" + add_error "UPDATE_PRECHECK_FAILED_PREREQUISITE_MISSING" let update_precheck_failed_conflict_present = - "UPDATE_PRECHECK_FAILED_CONFLICT_PRESENT" + add_error "UPDATE_PRECHECK_FAILED_CONFLICT_PRESENT" let update_precheck_failed_wrong_server_version = - "UPDATE_PRECHECK_FAILED_WRONG_SERVER_VERSION" + add_error "UPDATE_PRECHECK_FAILED_WRONG_SERVER_VERSION" let update_precheck_failed_gpgkey_not_imported = - "UPDATE_PRECHECK_FAILED_GPGKEY_NOT_IMPORTED" + add_error "UPDATE_PRECHECK_FAILED_GPGKEY_NOT_IMPORTED" -let patch_precheck_failed_unknown_error = "PATCH_PRECHECK_FAILED_UNKNOWN_ERROR" +let patch_precheck_failed_unknown_error = + add_error "PATCH_PRECHECK_FAILED_UNKNOWN_ERROR" let patch_precheck_failed_prerequisite_missing = - "PATCH_PRECHECK_FAILED_PREREQUISITE_MISSING" + add_error "PATCH_PRECHECK_FAILED_PREREQUISITE_MISSING" let patch_precheck_failed_wrong_server_version = - "PATCH_PRECHECK_FAILED_WRONG_SERVER_VERSION" + add_error "PATCH_PRECHECK_FAILED_WRONG_SERVER_VERSION" let patch_precheck_failed_wrong_server_build = - "PATCH_PRECHECK_FAILED_WRONG_SERVER_BUILD" + add_error "PATCH_PRECHECK_FAILED_WRONG_SERVER_BUILD" -let patch_precheck_failed_vm_running = "PATCH_PRECHECK_FAILED_VM_RUNNING" +let patch_precheck_failed_vm_running = + add_error "PATCH_PRECHECK_FAILED_VM_RUNNING" -let patch_precheck_failed_out_of_space = "PATCH_PRECHECK_FAILED_OUT_OF_SPACE" +let patch_precheck_failed_out_of_space = + add_error "PATCH_PRECHECK_FAILED_OUT_OF_SPACE" -let update_precheck_failed_out_of_space = "UPDATE_PRECHECK_FAILED_OUT_OF_SPACE" +let update_precheck_failed_out_of_space = + add_error "UPDATE_PRECHECK_FAILED_OUT_OF_SPACE" -let patch_precheck_tools_iso_mounted = "PATCH_PRECHECK_FAILED_ISO_MOUNTED" +let patch_precheck_tools_iso_mounted = + add_error "PATCH_PRECHECK_FAILED_ISO_MOUNTED" -let patch_apply_failed = "PATCH_APPLY_FAILED" +let patch_apply_failed = add_error "PATCH_APPLY_FAILED" let patch_apply_failed_backup_files_exist = - "PATCH_APPLY_FAILED_BACKUP_FILES_EXIST" + add_error "PATCH_APPLY_FAILED_BACKUP_FILES_EXIST" -let cannot_find_oem_backup_partition = "CANNOT_FIND_OEM_BACKUP_PARTITION" +let cannot_find_oem_backup_partition = + add_error "CANNOT_FIND_OEM_BACKUP_PARTITION" -let only_allowed_on_oem_edition = "ONLY_ALLOWED_ON_OEM_EDITION" +let only_allowed_on_oem_edition = add_error "ONLY_ALLOWED_ON_OEM_EDITION" -let not_allowed_on_oem_edition = "NOT_ALLOWED_ON_OEM_EDITION" +let not_allowed_on_oem_edition = add_error "NOT_ALLOWED_ON_OEM_EDITION" -let cannot_find_state_partition = "CANNOT_FIND_STATE_PARTITION" +let cannot_find_state_partition = add_error "CANNOT_FIND_STATE_PARTITION" -let backup_script_failed = "BACKUP_SCRIPT_FAILED" +let backup_script_failed = add_error "BACKUP_SCRIPT_FAILED" -let restore_script_failed = "RESTORE_SCRIPT_FAILED" +let restore_script_failed = add_error "RESTORE_SCRIPT_FAILED" -let license_expired = "LICENSE_EXPIRED" +let license_expired = add_error "LICENSE_EXPIRED" -let license_restriction = "LICENCE_RESTRICTION" +let license_restriction = add_error "LICENCE_RESTRICTION" -let license_does_not_support_pooling = "LICENSE_DOES_NOT_SUPPORT_POOLING" +let license_does_not_support_pooling = + add_error "LICENSE_DOES_NOT_SUPPORT_POOLING" -let license_host_pool_mismatch = "LICENSE_HOST_POOL_MISMATCH" +let license_host_pool_mismatch = add_error "LICENSE_HOST_POOL_MISMATCH" -let license_processing_error = "LICENSE_PROCESSING_ERROR" +let license_processing_error = add_error "LICENSE_PROCESSING_ERROR" -let license_cannot_downgrade_in_pool = "LICENSE_CANNOT_DOWNGRADE_WHILE_IN_POOL" +let license_cannot_downgrade_in_pool = + add_error "LICENSE_CANNOT_DOWNGRADE_WHILE_IN_POOL" -let license_does_not_support_xha = "LICENSE_DOES_NOT_SUPPORT_XHA" +let license_does_not_support_xha = add_error "LICENSE_DOES_NOT_SUPPORT_XHA" -let v6d_failure = "V6D_FAILURE" +let v6d_failure = add_error "V6D_FAILURE" -let invalid_edition = "INVALID_EDITION" +let invalid_edition = add_error "INVALID_EDITION" -let missing_connection_details = "MISSING_CONNECTION_DETAILS" +let missing_connection_details = add_error "MISSING_CONNECTION_DETAILS" -let license_checkout_error = "LICENSE_CHECKOUT_ERROR" +let license_checkout_error = add_error "LICENSE_CHECKOUT_ERROR" -let license_file_deprecated = "LICENSE_FILE_DEPRECATED" +let license_file_deprecated = add_error "LICENSE_FILE_DEPRECATED" -let activation_while_not_free = "ACTIVATION_WHILE_NOT_FREE" +let activation_while_not_free = add_error "ACTIVATION_WHILE_NOT_FREE" -let feature_restricted = "FEATURE_RESTRICTED" +let feature_restricted = add_error "FEATURE_RESTRICTED" -let xmlrpc_unmarshal_failure = "XMLRPC_UNMARSHAL_FAILURE" +let xmlrpc_unmarshal_failure = add_error "XMLRPC_UNMARSHAL_FAILURE" -let duplicate_vm = "DUPLICATE_VM" +let duplicate_vm = add_error "DUPLICATE_VM" -let duplicate_mac_seed = "DUPLICATE_MAC_SEED" +let duplicate_mac_seed = add_error "DUPLICATE_MAC_SEED" -let client_error = "CLIENT_ERROR" +let client_error = add_error "CLIENT_ERROR" -let ballooning_disabled = "BALLOONING_DISABLED" +let ballooning_disabled = add_error "BALLOONING_DISABLED" -let ballooning_timeout_before_migration = "BALLOONING_TIMEOUT_BEFORE_MIGRATION" +let ballooning_timeout_before_migration = + add_error "BALLOONING_TIMEOUT_BEFORE_MIGRATION" -let ha_host_is_armed = "HA_HOST_IS_ARMED" +let ha_host_is_armed = add_error "HA_HOST_IS_ARMED" -let ha_is_enabled = "HA_IS_ENABLED" +let ha_is_enabled = add_error "HA_IS_ENABLED" -let ha_not_enabled = "HA_NOT_ENABLED" +let ha_not_enabled = add_error "HA_NOT_ENABLED" -let ha_enable_in_progress = "HA_ENABLE_IN_PROGRESS" +let ha_enable_in_progress = add_error "HA_ENABLE_IN_PROGRESS" -let ha_disable_in_progress = "HA_DISABLE_IN_PROGRESS" +let ha_disable_in_progress = add_error "HA_DISABLE_IN_PROGRESS" -let ha_not_installed = "HA_NOT_INSTALLED" +let ha_not_installed = add_error "HA_NOT_INSTALLED" -let ha_host_cannot_see_peers = "HA_HOST_CANNOT_SEE_PEERS" +let ha_host_cannot_see_peers = add_error "HA_HOST_CANNOT_SEE_PEERS" -let ha_too_few_hosts = "HA_TOO_FEW_HOSTS" +let ha_too_few_hosts = add_error "HA_TOO_FEW_HOSTS" -let ha_should_be_fenced = "HA_SHOULD_BE_FENCED" +let ha_should_be_fenced = add_error "HA_SHOULD_BE_FENCED" -let ha_abort_new_master = "HA_ABORT_NEW_MASTER" +let ha_abort_new_master = add_error "HA_ABORT_NEW_MASTER" -let ha_no_plan = "HA_NO_PLAN" +let ha_no_plan = add_error "HA_NO_PLAN" -let ha_lost_statefile = "HA_LOST_STATEFILE" +let ha_lost_statefile = add_error "HA_LOST_STATEFILE" let ha_pool_is_enabled_but_host_is_disabled = - "HA_POOL_IS_ENABLED_BUT_HOST_IS_DISABLED" + add_error "HA_POOL_IS_ENABLED_BUT_HOST_IS_DISABLED" -let ha_heartbeat_daemon_startup_failed = "HA_HEARTBEAT_DAEMON_STARTUP_FAILED" +let ha_heartbeat_daemon_startup_failed = + add_error "HA_HEARTBEAT_DAEMON_STARTUP_FAILED" -let ha_host_cannot_access_statefile = "HA_HOST_CANNOT_ACCESS_STATEFILE" +let ha_host_cannot_access_statefile = + add_error "HA_HOST_CANNOT_ACCESS_STATEFILE" -let ha_failed_to_form_liveset = "HA_FAILED_TO_FORM_LIVESET" +let ha_failed_to_form_liveset = add_error "HA_FAILED_TO_FORM_LIVESET" let ha_cannot_change_bond_status_of_mgmt_iface = - "HA_CANNOT_CHANGE_BOND_STATUS_OF_MGMT_IFACE" + add_error "HA_CANNOT_CHANGE_BOND_STATUS_OF_MGMT_IFACE" (* CA-16480: prevent configuration errors which nullify xHA goodness *) let ha_constraint_violation_sr_not_shared = - "HA_CONSTRAINT_VIOLATION_SR_NOT_SHARED" + add_error "HA_CONSTRAINT_VIOLATION_SR_NOT_SHARED" let ha_constraint_violation_network_not_shared = - "HA_CONSTRAINT_VIOLATION_NETWORK_NOT_SHARED" + add_error "HA_CONSTRAINT_VIOLATION_NETWORK_NOT_SHARED" let ha_operation_would_break_failover_plan = - "HA_OPERATION_WOULD_BREAK_FAILOVER_PLAN" + add_error "HA_OPERATION_WOULD_BREAK_FAILOVER_PLAN" -let incompatible_statefile_sr = "INCOMPATIBLE_STATEFILE_SR" +let incompatible_statefile_sr = add_error "INCOMPATIBLE_STATEFILE_SR" -let incompatible_cluster_stack_active = "INCOMPATIBLE_CLUSTER_STACK_ACTIVE" +let incompatible_cluster_stack_active = + add_error "INCOMPATIBLE_CLUSTER_STACK_ACTIVE" -let cannot_evacuate_host = "CANNOT_EVACUATE_HOST" +let cannot_evacuate_host = add_error "CANNOT_EVACUATE_HOST" -let host_evacuate_in_progress = "HOST_EVACUATE_IN_PROGRESS" +let host_evacuate_in_progress = add_error "HOST_EVACUATE_IN_PROGRESS" -let system_status_retrieval_failed = "SYSTEM_STATUS_RETRIEVAL_FAILED" +let system_status_retrieval_failed = add_error "SYSTEM_STATUS_RETRIEVAL_FAILED" -let system_status_must_use_tar_on_oem = "SYSTEM_STATUS_MUST_USE_TAR_ON_OEM" +let system_status_must_use_tar_on_oem = + add_error "SYSTEM_STATUS_MUST_USE_TAR_ON_OEM" -let xapi_hook_failed = "XAPI_HOOK_FAILED" +let xapi_hook_failed = add_error "XAPI_HOOK_FAILED" -let no_local_storage = "NO_LOCAL_STORAGE" +let no_local_storage = add_error "NO_LOCAL_STORAGE" -let xenapi_missing_plugin = "XENAPI_MISSING_PLUGIN" +let xenapi_missing_plugin = add_error "XENAPI_MISSING_PLUGIN" -let xenapi_plugin_failure = "XENAPI_PLUGIN_FAILURE" +let xenapi_plugin_failure = add_error "XENAPI_PLUGIN_FAILURE" -let sr_attached = "SR_ATTACHED" +let sr_attached = add_error "SR_ATTACHED" -let sr_not_attached = "SR_NOT_ATTACHED" +let sr_not_attached = add_error "SR_NOT_ATTACHED" -let domain_builder_error = "DOMAIN_BUILDER_ERROR" +let domain_builder_error = add_error "DOMAIN_BUILDER_ERROR" -let auth_already_enabled = "AUTH_ALREADY_ENABLED" +let auth_already_enabled = add_error "AUTH_ALREADY_ENABLED" -let auth_unknown_type = "AUTH_UNKNOWN_TYPE" +let auth_unknown_type = add_error "AUTH_UNKNOWN_TYPE" -let auth_is_disabled = "AUTH_IS_DISABLED" +let auth_is_disabled = add_error "AUTH_IS_DISABLED" let auth_suffix_wrong_credentials = "_WRONG_CREDENTIALS" @@ -969,333 +1027,351 @@ let auth_suffix_invalid_ou = "_INVALID_OU" let auth_suffix_invalid_account = "_INVALID_ACCOUNT" -let auth_enable_failed = "AUTH_ENABLE_FAILED" +let auth_enable_failed = add_error "AUTH_ENABLE_FAILED" let auth_enable_failed_wrong_credentials = - auth_enable_failed ^ auth_suffix_wrong_credentials + add_error $ auth_enable_failed ^ auth_suffix_wrong_credentials let auth_enable_failed_permission_denied = - auth_enable_failed ^ auth_suffix_permission_denied + add_error $ auth_enable_failed ^ auth_suffix_permission_denied let auth_enable_failed_domain_lookup_failed = - auth_enable_failed ^ auth_suffix_domain_lookup_failed + add_error $ auth_enable_failed ^ auth_suffix_domain_lookup_failed let auth_enable_failed_unavailable = - auth_enable_failed ^ auth_suffix_unavailable + add_error $ auth_enable_failed ^ auth_suffix_unavailable -let auth_enable_failed_invalid_ou = auth_enable_failed ^ auth_suffix_invalid_ou +let auth_enable_failed_invalid_ou = + add_error $ auth_enable_failed ^ auth_suffix_invalid_ou let auth_enable_failed_invalid_account = - auth_enable_failed ^ auth_suffix_invalid_account + add_error $ auth_enable_failed ^ auth_suffix_invalid_account -let auth_disable_failed = "AUTH_DISABLE_FAILED" +let auth_disable_failed = add_error "AUTH_DISABLE_FAILED" let auth_disable_failed_wrong_credentials = - auth_disable_failed ^ auth_suffix_wrong_credentials + add_error $ auth_disable_failed ^ auth_suffix_wrong_credentials let auth_disable_failed_permission_denied = - auth_disable_failed ^ auth_suffix_permission_denied + add_error $ auth_disable_failed ^ auth_suffix_permission_denied -let pool_auth_already_enabled = "POOL_AUTH_ALREADY_ENABLED" +let pool_auth_already_enabled = add_error "POOL_AUTH_ALREADY_ENABLED" let pool_auth_prefix = "POOL_" -let pool_auth_enable_failed = pool_auth_prefix ^ auth_enable_failed +let pool_auth_enable_failed = add_error $ pool_auth_prefix ^ auth_enable_failed let pool_auth_enable_failed_wrong_credentials = - pool_auth_enable_failed ^ auth_suffix_wrong_credentials + add_error $ pool_auth_enable_failed ^ auth_suffix_wrong_credentials let pool_auth_enable_failed_permission_denied = - pool_auth_enable_failed ^ auth_suffix_permission_denied + add_error $ pool_auth_enable_failed ^ auth_suffix_permission_denied let pool_auth_enable_failed_domain_lookup_failed = - pool_auth_enable_failed ^ auth_suffix_domain_lookup_failed + add_error $ pool_auth_enable_failed ^ auth_suffix_domain_lookup_failed let pool_auth_enable_failed_unavailable = - pool_auth_enable_failed ^ auth_suffix_unavailable + add_error $ pool_auth_enable_failed ^ auth_suffix_unavailable let pool_auth_enable_failed_invalid_ou = - pool_auth_enable_failed ^ auth_suffix_invalid_ou + add_error $ pool_auth_enable_failed ^ auth_suffix_invalid_ou let pool_auth_enable_failed_invalid_account = - pool_auth_enable_failed ^ auth_suffix_invalid_account + add_error $ pool_auth_enable_failed ^ auth_suffix_invalid_account let pool_auth_enable_failed_duplicate_hostname = - pool_auth_enable_failed ^ "_DUPLICATE_HOSTNAME" + add_error $ pool_auth_enable_failed ^ "_DUPLICATE_HOSTNAME" -let pool_auth_disable_failed = pool_auth_prefix ^ auth_disable_failed +let pool_auth_disable_failed = + add_error $ pool_auth_prefix ^ auth_disable_failed let pool_auth_disable_failed_wrong_credentials = - pool_auth_disable_failed ^ auth_suffix_wrong_credentials + add_error $ pool_auth_disable_failed ^ auth_suffix_wrong_credentials let pool_auth_disable_failed_permission_denied = - pool_auth_disable_failed ^ auth_suffix_permission_denied + add_error $ pool_auth_disable_failed ^ auth_suffix_permission_denied let pool_auth_disable_failed_invalid_account = - pool_auth_disable_failed ^ auth_suffix_invalid_account + add_error $ pool_auth_disable_failed ^ auth_suffix_invalid_account -let subject_cannot_be_resolved = "SUBJECT_CANNOT_BE_RESOLVED" +let subject_cannot_be_resolved = add_error "SUBJECT_CANNOT_BE_RESOLVED" -let auth_service_error = "AUTH_SERVICE_ERROR" +let auth_service_error = add_error "AUTH_SERVICE_ERROR" -let subject_already_exists = "SUBJECT_ALREADY_EXISTS" +let subject_already_exists = add_error "SUBJECT_ALREADY_EXISTS" -let role_not_found = "ROLE_NOT_FOUND" +let role_not_found = add_error "ROLE_NOT_FOUND" -let role_already_exists = "ROLE_ALREADY_EXISTS" +let role_already_exists = add_error "ROLE_ALREADY_EXISTS" -let rbac_permission_denied = "RBAC_PERMISSION_DENIED" +let rbac_permission_denied = add_error "RBAC_PERMISSION_DENIED" -let certificate_does_not_exist = "CERTIFICATE_DOES_NOT_EXIST" +let certificate_does_not_exist = add_error "CERTIFICATE_DOES_NOT_EXIST" -let certificate_already_exists = "CERTIFICATE_ALREADY_EXISTS" +let certificate_already_exists = add_error "CERTIFICATE_ALREADY_EXISTS" -let certificate_name_invalid = "CERTIFICATE_NAME_INVALID" +let certificate_name_invalid = add_error "CERTIFICATE_NAME_INVALID" -let certificate_corrupt = "CERTIFICATE_CORRUPT" +let certificate_corrupt = add_error "CERTIFICATE_CORRUPT" -let certificate_library_corrupt = "CERTIFICATE_LIBRARY_CORRUPT" +let certificate_library_corrupt = add_error "CERTIFICATE_LIBRARY_CORRUPT" -let crl_does_not_exist = "CRL_DOES_NOT_EXIST" +let crl_does_not_exist = add_error "CRL_DOES_NOT_EXIST" -let crl_already_exists = "CRL_ALREADY_EXISTS" +let crl_already_exists = add_error "CRL_ALREADY_EXISTS" -let crl_name_invalid = "CRL_NAME_INVALID" +let crl_name_invalid = add_error "CRL_NAME_INVALID" -let crl_corrupt = "CRL_CORRUPT" +let crl_corrupt = add_error "CRL_CORRUPT" -let server_certificate_key_invalid = "SERVER_CERTIFICATE_KEY_INVALID" +let server_certificate_key_invalid = add_error "SERVER_CERTIFICATE_KEY_INVALID" let server_certificate_key_algorithm_not_supported = - "SERVER_CERTIFICATE_KEY_ALGORITHM_NOT_SUPPORTED" + add_error "SERVER_CERTIFICATE_KEY_ALGORITHM_NOT_SUPPORTED" let server_certificate_key_rsa_length_not_supported = - "SERVER_CERTIFICATE_KEY_RSA_LENGTH_NOT_SUPPORTED" + add_error "SERVER_CERTIFICATE_KEY_RSA_LENGTH_NOT_SUPPORTED" let server_certificate_key_rsa_multi_not_supported = - "SERVER_CERTIFICATE_KEY_RSA_MULTI_NOT_SUPPORTED" + add_error "SERVER_CERTIFICATE_KEY_RSA_MULTI_NOT_SUPPORTED" -let server_certificate_invalid = "SERVER_CERTIFICATE_INVALID" +let server_certificate_invalid = add_error "SERVER_CERTIFICATE_INVALID" -let ca_certificate_invalid = "CA_CERTIFICATE_INVALID" +let ca_certificate_invalid = add_error "CA_CERTIFICATE_INVALID" -let server_certificate_key_mismatch = "SERVER_CERTIFICATE_KEY_MISMATCH" +let server_certificate_key_mismatch = + add_error "SERVER_CERTIFICATE_KEY_MISMATCH" -let server_certificate_not_valid_yet = "SERVER_CERTIFICATE_NOT_VALID_YET" +let server_certificate_not_valid_yet = + add_error "SERVER_CERTIFICATE_NOT_VALID_YET" -let ca_certificate_not_valid_yet = "CA_CERTIFICATE_NOT_VALID_YET" +let ca_certificate_not_valid_yet = add_error "CA_CERTIFICATE_NOT_VALID_YET" -let server_certificate_expired = "SERVER_CERTIFICATE_EXPIRED" +let server_certificate_expired = add_error "SERVER_CERTIFICATE_EXPIRED" -let ca_certificate_expired = "CA_CERTIFICATE_EXPIRED" +let ca_certificate_expired = add_error "CA_CERTIFICATE_EXPIRED" let server_certificate_signature_not_supported = - "SERVER_CERTIFICATE_SIGNATURE_NOT_SUPPORTED" + add_error "SERVER_CERTIFICATE_SIGNATURE_NOT_SUPPORTED" -let server_certificate_chain_invalid = "SERVER_CERTIFICATE_CHAIN_INVALID" +let server_certificate_chain_invalid = + add_error "SERVER_CERTIFICATE_CHAIN_INVALID" -let vmpp_has_vm = "VMPP_HAS_VM" +let vmpp_has_vm = add_error "VMPP_HAS_VM" let vmpp_archive_more_frequent_than_backup = - "VMPP_ARCHIVE_MORE_FREQUENT_THAN_BACKUP" + add_error "VMPP_ARCHIVE_MORE_FREQUENT_THAN_BACKUP" -let vm_assigned_to_protection_policy = "VM_ASSIGNED_TO_PROTECTION_POLICY" +let vm_assigned_to_protection_policy = + add_error "VM_ASSIGNED_TO_PROTECTION_POLICY" -let vmss_has_vm = "VMSS_HAS_VM" +let vmss_has_vm = add_error "VMSS_HAS_VM" -let vm_assigned_to_snapshot_schedule = "VM_ASSIGNED_TO_SNAPSHOT_SCHEDULE" +let vm_assigned_to_snapshot_schedule = + add_error "VM_ASSIGNED_TO_SNAPSHOT_SCHEDULE" -let ssl_verify_error = "SSL_VERIFY_ERROR" +let ssl_verify_error = add_error "SSL_VERIFY_ERROR" -let cannot_enable_redo_log = "CANNOT_ENABLE_REDO_LOG" +let cannot_enable_redo_log = add_error "CANNOT_ENABLE_REDO_LOG" -let redo_log_is_enabled = "REDO_LOG_IS_ENABLED" +let redo_log_is_enabled = add_error "REDO_LOG_IS_ENABLED" -let vm_bios_strings_already_set = "VM_BIOS_STRINGS_ALREADY_SET" +let vm_bios_strings_already_set = add_error "VM_BIOS_STRINGS_ALREADY_SET" -let invalid_feature_string = "INVALID_FEATURE_STRING" +let invalid_feature_string = add_error "INVALID_FEATURE_STRING" -let cpu_feature_masking_not_supported = "CPU_FEATURE_MASKING_NOT_SUPPORTED" +let cpu_feature_masking_not_supported = + add_error "CPU_FEATURE_MASKING_NOT_SUPPORTED" -let feature_requires_hvm = "FEATURE_REQUIRES_HVM" +let feature_requires_hvm = add_error "FEATURE_REQUIRES_HVM" (* Disaster recovery *) -let vdi_contains_metadata_of_this_pool = "VDI_CONTAINS_METADATA_OF_THIS_POOL" +let vdi_contains_metadata_of_this_pool = + add_error "VDI_CONTAINS_METADATA_OF_THIS_POOL" -let no_more_redo_logs_allowed = "NO_MORE_REDO_LOGS_ALLOWED" +let no_more_redo_logs_allowed = add_error "NO_MORE_REDO_LOGS_ALLOWED" -let could_not_import_database = "COULD_NOT_IMPORT_DATABASE" +let could_not_import_database = add_error "COULD_NOT_IMPORT_DATABASE" -let vm_incompatible_with_this_host = "VM_INCOMPATIBLE_WITH_THIS_HOST" +let vm_incompatible_with_this_host = add_error "VM_INCOMPATIBLE_WITH_THIS_HOST" let cannot_destroy_disaster_recovery_task = - "CANNOT_DESTROY_DISASTER_RECOVERY_TASK" + add_error "CANNOT_DESTROY_DISASTER_RECOVERY_TASK" -let vm_is_part_of_an_appliance = "VM_IS_PART_OF_AN_APPLIANCE" +let vm_is_part_of_an_appliance = add_error "VM_IS_PART_OF_AN_APPLIANCE" -let vm_to_import_is_not_newer_version = "VM_TO_IMPORT_IS_NOT_NEWER_VERSION" +let vm_to_import_is_not_newer_version = + add_error "VM_TO_IMPORT_IS_NOT_NEWER_VERSION" let suspend_vdi_replacement_is_not_identical = - "SUSPEND_VDI_REPLACEMENT_IS_NOT_IDENTICAL" + add_error "SUSPEND_VDI_REPLACEMENT_IS_NOT_IDENTICAL" -let vdi_copy_failed = "VDI_COPY_FAILED" +let vdi_copy_failed = add_error "VDI_COPY_FAILED" -let vdi_needs_vm_for_migrate = "VDI_NEEDS_VM_FOR_MIGRATE" +let vdi_needs_vm_for_migrate = add_error "VDI_NEEDS_VM_FOR_MIGRATE" -let vm_has_too_many_snapshots = "VM_HAS_TOO_MANY_SNAPSHOTS" +let vm_has_too_many_snapshots = add_error "VM_HAS_TOO_MANY_SNAPSHOTS" -let vm_has_checkpoint = "VM_HAS_CHECKPOINT" +let vm_has_checkpoint = add_error "VM_HAS_CHECKPOINT" -let mirror_failed = "MIRROR_FAILED" +let mirror_failed = add_error "MIRROR_FAILED" -let too_many_storage_migrates = "TOO_MANY_STORAGE_MIGRATES" +let too_many_storage_migrates = add_error "TOO_MANY_STORAGE_MIGRATES" -let sr_does_not_support_migration = "SR_DOES_NOT_SUPPORT_MIGRATION" +let sr_does_not_support_migration = add_error "SR_DOES_NOT_SUPPORT_MIGRATION" -let unimplemented_in_sm_backend = "UNIMPLEMENTED_IN_SM_BACKEND" +let unimplemented_in_sm_backend = add_error "UNIMPLEMENTED_IN_SM_BACKEND" -let vm_call_plugin_rate_limit = "VM_CALL_PLUGIN_RATE_LIMIT" +let vm_call_plugin_rate_limit = add_error "VM_CALL_PLUGIN_RATE_LIMIT" -let suspend_image_not_accessible = "SUSPEND_IMAGE_NOT_ACCESSIBLE" +let suspend_image_not_accessible = add_error "SUSPEND_IMAGE_NOT_ACCESSIBLE" (* PVS *) -let pvs_site_contains_running_proxies = "PVS_SITE_CONTAINS_RUNNING_PROXIES" +let pvs_site_contains_running_proxies = + add_error "PVS_SITE_CONTAINS_RUNNING_PROXIES" -let pvs_site_contains_servers = "PVS_SITE_CONTAINS_SERVERS" +let pvs_site_contains_servers = add_error "PVS_SITE_CONTAINS_SERVERS" -let pvs_cache_storage_already_present = "PVS_CACHE_STORAGE_ALREADY_PRESENT" +let pvs_cache_storage_already_present = + add_error "PVS_CACHE_STORAGE_ALREADY_PRESENT" -let pvs_cache_storage_is_in_use = "PVS_CACHE_STORAGE_IS_IN_USE" +let pvs_cache_storage_is_in_use = add_error "PVS_CACHE_STORAGE_IS_IN_USE" -let pvs_proxy_already_present = "PVS_PROXY_ALREADY_PRESENT" +let pvs_proxy_already_present = add_error "PVS_PROXY_ALREADY_PRESENT" -let pvs_server_address_in_use = "PVS_SERVER_ADDRESS_IN_USE" +let pvs_server_address_in_use = add_error "PVS_SERVER_ADDRESS_IN_USE" -let extension_protocol_failure = "EXTENSION_PROTOCOL_FAILURE" +let extension_protocol_failure = add_error "EXTENSION_PROTOCOL_FAILURE" -let usb_group_contains_vusb = "USB_GROUP_CONTAINS_VUSB" +let usb_group_contains_vusb = add_error "USB_GROUP_CONTAINS_VUSB" -let usb_group_contains_pusb = "USB_GROUP_CONTAINS_PUSB" +let usb_group_contains_pusb = add_error "USB_GROUP_CONTAINS_PUSB" -let usb_group_contains_no_pusbs = "USB_GROUP_CONTAINS_NO_PUSBS" +let usb_group_contains_no_pusbs = add_error "USB_GROUP_CONTAINS_NO_PUSBS" -let usb_group_conflict = "USB_GROUP_CONFLICT" +let usb_group_conflict = add_error "USB_GROUP_CONFLICT" -let usb_already_attached = "USB_ALREADY_ATTACHED" +let usb_already_attached = add_error "USB_ALREADY_ATTACHED" -let too_many_vusbs = "TOO_MANY_VUSBS" +let too_many_vusbs = add_error "TOO_MANY_VUSBS" -let pusb_vdi_conflict = "PUSB_VDI_CONFLICT" +let pusb_vdi_conflict = add_error "PUSB_VDI_CONFLICT" -let vm_has_vusbs = "VM_HAS_VUSBS" +let vm_has_vusbs = add_error "VM_HAS_VUSBS" -let cluster_has_no_certificate = "CLUSTER_HAS_NO_CERTIFICATE" +let cluster_has_no_certificate = add_error "CLUSTER_HAS_NO_CERTIFICATE" -let cluster_create_in_progress = "CLUSTER_CREATE_IN_PROGRESS" +let cluster_create_in_progress = add_error "CLUSTER_CREATE_IN_PROGRESS" -let cluster_already_exists = "CLUSTER_ALREADY_EXISTS" +let cluster_already_exists = add_error "CLUSTER_ALREADY_EXISTS" -let clustering_enabled = "CLUSTERING_ENABLED" +let clustering_enabled = add_error "CLUSTERING_ENABLED" -let clustering_disabled = "CLUSTERING_DISABLED" +let clustering_disabled = add_error "CLUSTERING_DISABLED" -let cluster_does_not_have_one_node = "CLUSTER_DOES_NOT_HAVE_ONE_NODE" +let cluster_does_not_have_one_node = add_error "CLUSTER_DOES_NOT_HAVE_ONE_NODE" -let cluster_host_is_last = "CLUSTER_HOST_IS_LAST" +let cluster_host_is_last = add_error "CLUSTER_HOST_IS_LAST" -let no_compatible_cluster_host = "NO_COMPATIBLE_CLUSTER_HOST" +let no_compatible_cluster_host = add_error "NO_COMPATIBLE_CLUSTER_HOST" -let cluster_force_destroy_failed = "CLUSTER_FORCE_DESTROY_FAILED" +let cluster_force_destroy_failed = add_error "CLUSTER_FORCE_DESTROY_FAILED" -let cluster_stack_in_use = "CLUSTER_STACK_IN_USE" +let cluster_stack_in_use = add_error "CLUSTER_STACK_IN_USE" -let invalid_cluster_stack = "INVALID_CLUSTER_STACK" +let invalid_cluster_stack = add_error "INVALID_CLUSTER_STACK" -let pif_not_attached_to_host = "PIF_NOT_ATTACHED_TO_HOST" +let pif_not_attached_to_host = add_error "PIF_NOT_ATTACHED_TO_HOST" -let cluster_host_not_joined = "CLUSTER_HOST_NOT_JOINED" +let cluster_host_not_joined = add_error "CLUSTER_HOST_NOT_JOINED" -let no_cluster_hosts_reachable = "NO_CLUSTER_HOSTS_REACHABLE" +let no_cluster_hosts_reachable = add_error "NO_CLUSTER_HOSTS_REACHABLE" -let xen_incompatible = "XEN_INCOMPATIBLE" +let xen_incompatible = add_error "XEN_INCOMPATIBLE" let vcpu_max_not_cores_per_socket_multiple = - "VCPU_MAX_NOT_CORES_PER_SOCKET_MULTIPLE" + add_error "VCPU_MAX_NOT_CORES_PER_SOCKET_MULTIPLE" -let designate_new_master_in_progress = "DESIGNATE_NEW_MASTER_IN_PROGRESS" +let designate_new_master_in_progress = + add_error "DESIGNATE_NEW_MASTER_IN_PROGRESS" -let pool_secret_rotation_pending = "POOL_SECRET_ROTATION_PENDING" +let pool_secret_rotation_pending = add_error "POOL_SECRET_ROTATION_PENDING" -let tls_verification_enable_in_progress = "TLS_VERIFICATION_ENABLE_IN_PROGRESS" +let tls_verification_enable_in_progress = + add_error "TLS_VERIFICATION_ENABLE_IN_PROGRESS" -let cert_refresh_in_progress = "CERT_REFRESH_IN_PROGRESS" +let cert_refresh_in_progress = add_error "CERT_REFRESH_IN_PROGRESS" -let configure_repositories_in_progress = "CONFIGURE_REPOSITORIES_IN_PROGRESS" +let configure_repositories_in_progress = + add_error "CONFIGURE_REPOSITORIES_IN_PROGRESS" -let invalid_base_url = "INVALID_BASE_URL" +let invalid_base_url = add_error "INVALID_BASE_URL" -let invalid_gpgkey_path = "INVALID_GPGKEY_PATH" +let invalid_gpgkey_path = add_error "INVALID_GPGKEY_PATH" -let repository_already_exists = "REPOSITORY_ALREADY_EXISTS" +let repository_already_exists = add_error "REPOSITORY_ALREADY_EXISTS" -let repository_is_in_use = "REPOSITORY_IS_IN_USE" +let repository_is_in_use = add_error "REPOSITORY_IS_IN_USE" -let repository_cleanup_failed = "REPOSITORY_CLEANUP_FAILED" +let repository_cleanup_failed = add_error "REPOSITORY_CLEANUP_FAILED" -let no_repository_enabled = "NO_REPOSITORY_ENABLED" +let no_repository_enabled = add_error "NO_REPOSITORY_ENABLED" let multiple_update_repositories_enabled = - "MULTIPLE_UPDATE_REPOSITORIES_ENABLED" + add_error "MULTIPLE_UPDATE_REPOSITORIES_ENABLED" -let sync_updates_in_progress = "SYNC_UPDATES_IN_PROGRESS" +let sync_updates_in_progress = add_error "SYNC_UPDATES_IN_PROGRESS" -let reposync_failed = "REPOSYNC_FAILED" +let reposync_failed = add_error "REPOSYNC_FAILED" -let createrepo_failed = "CREATEREPO_FAILED" +let createrepo_failed = add_error "CREATEREPO_FAILED" -let invalid_updateinfo_xml = "INVALID_UPDATEINFO_XML" +let invalid_updateinfo_xml = add_error "INVALID_UPDATEINFO_XML" -let get_host_updates_failed = "GET_HOST_UPDATES_FAILED" +let get_host_updates_failed = add_error "GET_HOST_UPDATES_FAILED" -let invalid_repomd_xml = "INVALID_REPOMD_XML" +let invalid_repomd_xml = add_error "INVALID_REPOMD_XML" -let get_updates_failed = "GET_UPDATES_FAILED" +let get_updates_failed = add_error "GET_UPDATES_FAILED" -let get_updates_in_progress = "GET_UPDATES_IN_PROGRESS" +let get_updates_in_progress = add_error "GET_UPDATES_IN_PROGRESS" -let apply_updates_in_progress = "APPLY_UPDATES_IN_PROGRESS" +let apply_updates_in_progress = add_error "APPLY_UPDATES_IN_PROGRESS" -let apply_updates_failed = "APPLY_UPDATES_FAILED" +let apply_updates_failed = add_error "APPLY_UPDATES_FAILED" -let apply_guidance_failed = "APPLY_GUIDANCE_FAILED" +let apply_guidance_failed = add_error "APPLY_GUIDANCE_FAILED" -let updateinfo_hash_mismatch = "UPDATEINFO_HASH_MISMATCH" +let updateinfo_hash_mismatch = add_error "UPDATEINFO_HASH_MISMATCH" -let cannot_restart_device_model = "CANNOT_RESTART_DEVICE_MODEL" +let cannot_restart_device_model = add_error "CANNOT_RESTART_DEVICE_MODEL" -let invalid_repository_proxy_url = "INVALID_REPOSITORY_PROXY_URL" +let invalid_repository_proxy_url = add_error "INVALID_REPOSITORY_PROXY_URL" -let invalid_repository_proxy_credential = "INVALID_REPOSITORY_PROXY_CREDENTIAL" +let invalid_repository_proxy_credential = + add_error "INVALID_REPOSITORY_PROXY_CREDENTIAL" -let invalid_repository_domain_allowlist = "INVALID_REPOSITORY_DOMAIN_ALLOWLIST" +let invalid_repository_domain_allowlist = + add_error "INVALID_REPOSITORY_DOMAIN_ALLOWLIST" -let apply_livepatch_failed = "APPLY_LIVEPATCH_FAILED" +let apply_livepatch_failed = add_error "APPLY_LIVEPATCH_FAILED" -let invalid_update_sync_day = "INVALID_UPDATE_SYNC_DAY" +let invalid_update_sync_day = add_error "INVALID_UPDATE_SYNC_DAY" -let no_repositories_configured = "NO_REPOSITORIES_CONFIGURED" +let no_repositories_configured = add_error "NO_REPOSITORIES_CONFIGURED" let host_pending_mandatory_guidances_not_empty = - "HOST_PENDING_MANDATORY_GUIDANCE_NOT_EMPTY" + add_error "HOST_PENDING_MANDATORY_GUIDANCE_NOT_EMPTY" -let host_evacuation_is_required = "HOST_EVACUATION_IS_REQUIRED" +let host_evacuation_is_required = add_error "HOST_EVACUATION_IS_REQUIRED" (* VTPMs *) -let vtpm_max_amount_reached = "VTPM_MAX_AMOUNT_REACHED" +let vtpm_max_amount_reached = add_error "VTPM_MAX_AMOUNT_REACHED" (* Telemetry *) -let telemetry_next_collection_too_late = "TELEMETRY_NEXT_COLLECTION_TOO_LATE" +let telemetry_next_collection_too_late = + add_error "TELEMETRY_NEXT_COLLECTION_TOO_LATE" (* FIPS/CC_PREPARATIONS *) -let illegal_in_fips_mode = "ILLEGAL_IN_FIPS_MODE" +let illegal_in_fips_mode = add_error "ILLEGAL_IN_FIPS_MODE" From 2039c12ee199fb64f05d42f0131a94c4dc81e431 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 8 Apr 2024 20:33:24 +0800 Subject: [PATCH 07/99] CP-47364: generate api messages and errors of Golang code Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/autogen/src/go.mod | 3 ++ ocaml/sdk-gen/go/dune | 1 + ocaml/sdk-gen/go/gen_go_binding.ml | 20 +++++++++++ ocaml/sdk-gen/go/gen_go_helper.ml | 6 ++++ ocaml/sdk-gen/go/gen_go_helper.mli | 4 +++ ocaml/sdk-gen/go/test_data/api_errors.go | 6 ++++ ocaml/sdk-gen/go/test_data/api_messages.go | 6 ++++ ocaml/sdk-gen/go/test_gen_go.ml | 41 +++++++++++++++++++++- 8 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 ocaml/sdk-gen/go/autogen/src/go.mod create mode 100644 ocaml/sdk-gen/go/test_data/api_errors.go create mode 100644 ocaml/sdk-gen/go/test_data/api_messages.go diff --git a/ocaml/sdk-gen/go/autogen/src/go.mod b/ocaml/sdk-gen/go/autogen/src/go.mod new file mode 100644 index 00000000000..0d33115cf62 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/src/go.mod @@ -0,0 +1,3 @@ +module go/xenapi + +go 1.22.0 diff --git a/ocaml/sdk-gen/go/dune b/ocaml/sdk-gen/go/dune index f481b389530..00d835053bf 100644 --- a/ocaml/sdk-gen/go/dune +++ b/ocaml/sdk-gen/go/dune @@ -35,5 +35,6 @@ (libraries alcotest xapi-test-utils gen_go_helper) (deps (source_tree test_data) + (source_tree templates) ) ) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 2606be80ab7..bebd0ef9513 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -13,7 +13,27 @@ open Gen_go_helper +let render_api_messages_and_errors () = + let obj = + `O + [ + ("api_errors", `A Json.api_errors) + ; ("api_messages", `A Json.api_messages) + ; ("modules", `Null) + ] + in + let header = render_template "FileHeader.mustache" obj ^ "\n" in + let error_rendered = + header ^ render_template "APIErrors.mustache" obj ^ "\n" + in + let messages_rendered = + header ^ render_template "APIMessages.mustache" obj ^ "\n" + in + generate_file error_rendered "api_errors.go" ; + generate_file messages_rendered "api_messages.go" + let main () = + render_api_messages_and_errors () ; let objects = Json.xenapi objects in List.iter (fun (name, obj) -> diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index a2ea4503e31..6946a4c8436 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -269,6 +269,12 @@ module Json = struct (String.lowercase_ascii obj.name, `O assoc_list) ) objs + + let api_messages = + List.map (fun (msg, _) -> `O [("name", `String msg)]) !Api_messages.msgList + + let api_errors = + List.map (fun error -> `O [("name", `String error)]) !Api_errors.errors end let objects = diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index b88d0348498..d4ab0d416ef 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -25,4 +25,8 @@ val generate_file : string -> string -> unit module Json : sig val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list + + val api_messages : Mustache.Json.value list + + val api_errors : Mustache.Json.value list end diff --git a/ocaml/sdk-gen/go/test_data/api_errors.go b/ocaml/sdk-gen/go/test_data/api_errors.go new file mode 100644 index 00000000000..b14349c1885 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/api_errors.go @@ -0,0 +1,6 @@ +const ( + // + ERR_MESSAGE_DEPRECATED = "MESSAGE_DEPRECATED" + // + ERR_MESSAGE_REMOVED = "MESSAGE_REMOVED" +) diff --git a/ocaml/sdk-gen/go/test_data/api_messages.go b/ocaml/sdk-gen/go/test_data/api_messages.go new file mode 100644 index 00000000000..f91592ec010 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/api_messages.go @@ -0,0 +1,6 @@ +const ( + // + MSG_HA_STATEFILE_LOST = "HA_STATEFILE_LOST" + // + MSG_METADATA_LUN_HEALTHY = "METADATA_LUN_HEALTHY" +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index a93250ed3bc..32ecb0409b9 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -208,6 +208,30 @@ let enums : Mustache.Json.t = ) ] +let api_errors : Mustache.Json.t = + `O + [ + ( "api_errors" + , `A + [ + `O [("name", `String "MESSAGE_DEPRECATED")] + ; `O [("name", `String "MESSAGE_REMOVED")] + ] + ) + ] + +let api_messages : Mustache.Json.t = + `O + [ + ( "api_messages" + , `A + [ + `O [("name", `String "HA_STATEFILE_LOST")] + ; `O [("name", `String "METADATA_LUN_HEALTHY")] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -228,12 +252,18 @@ module TemplatesTest = Generic.MakeStateless (struct let enums_rendered = string_of_file "enum.go" + let api_errors_rendered = string_of_file "api_errors.go" + + let api_messages_rendered = string_of_file "api_messages.go" + let tests = `QuickAndAutoDocumented [ (("FileHeader.mustache", header), file_header_rendered) ; (("Record.mustache", record), record_rendered) ; (("Enum.mustache", enums), enums_rendered) + ; (("APIErrors.mustache", api_errors), api_errors_rendered) + ; (("APIMessages.mustache", api_messages), api_messages_rendered) ] end) @@ -251,7 +281,16 @@ let generated_json_tests = check_true "Mustache.Json of records has right structure" @@ List.for_all (fun (_, obj) -> is_same_struct obj json) objects in - [("jsons", `Quick, jsons)] + let errors_and_messages () = + let errors = `O [("api_errors", `A Json.api_errors)] in + let messages = `O [("api_messages", `A Json.api_messages)] in + check_true "Mustache.Json of errors and messages has right structure" + @@ (is_same_struct errors api_errors && is_same_struct messages api_messages) + in + [ + ("jsons", `Quick, jsons) + ; ("errors_and_messages", `Quick, errors_and_messages) + ] let tests = make_suite "gen_go_binding_" From dfc46a7113d0f225b7c7bd63e7ced4825804cba8 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 9 Apr 2024 14:34:15 +0800 Subject: [PATCH 08/99] refactor: create an `Alcotest.testable` to check structure of generated JSON is wanted Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 32ecb0409b9..7380e93ab60 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -20,8 +20,6 @@ let test_data_dir = "test_data" let string_of_file filename = string_of_file (test_data_dir // filename) |> String.trim -let check_true str = Alcotest.(check bool) str true - module SnakeToCamelTest = Generic.MakeStateless (struct module Io = struct type input_t = string @@ -267,37 +265,47 @@ module TemplatesTest = Generic.MakeStateless (struct ] end) -let generated_json_tests = +module TestGeneratedJson = struct let merge (obj1 : Mustache.Json.t) (obj2 : Mustache.Json.t) = match (obj1, obj2) with | `O list1, `O list2 -> `O (list1 @ list2) | _ -> `O [] - in - let jsons () = - let json = enums |> merge record |> merge header in - let objects = Json.xenapi objects in - check_true "Mustache.Json of records has right structure" - @@ List.for_all (fun (_, obj) -> is_same_struct obj json) objects - in - let errors_and_messages () = + + let pp_json = Fmt.of_to_string string_of_json + + let generated_json = Alcotest.testable pp_json is_same_struct + + let verify description actual expected = + Alcotest.(check @@ generated_json) description expected actual + + let json = enums |> merge record |> merge header + + let testing (name, obj) expected () = verify name obj expected + + let test_case ((name, _) as test_case) expected = + (name, `Quick, testing test_case expected) + + let errors_and_messages_tests = let errors = `O [("api_errors", `A Json.api_errors)] in let messages = `O [("api_messages", `A Json.api_messages)] in - check_true "Mustache.Json of errors and messages has right structure" - @@ (is_same_struct errors api_errors && is_same_struct messages api_messages) - in - [ - ("jsons", `Quick, jsons) - ; ("errors_and_messages", `Quick, errors_and_messages) - ] + [ + test_case ("api_errors", errors) api_errors + ; test_case ("api_messages", messages) api_messages + ] + + let tests = + (objects |> Json.xenapi |> List.map (fun obj -> test_case obj json)) + @ errors_and_messages_tests +end let tests = make_suite "gen_go_binding_" [ ("snake_to_camel", SnakeToCamelTest.tests) ; ("templates", TemplatesTest.tests) - ; ("generated_mustache_jsons", generated_json_tests) + ; ("generated_mustache_jsons", TestGeneratedJson.tests) ] let () = Alcotest.run "Gen go binding" tests From 6717121e57a519e1d4081bd41a6e9965ab207bd2 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 9 Apr 2024 14:39:16 +0800 Subject: [PATCH 09/99] refactor: move `objects` and `session_id` from `Gen_go_helper` to `CommonFunctions` Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 27 ++++++++ ocaml/sdk-gen/common/CommonFunctions.mli | 8 +++ ocaml/sdk-gen/go/gen_go_binding.ml | 1 + ocaml/sdk-gen/go/gen_go_helper.ml | 81 ++++++++---------------- ocaml/sdk-gen/go/gen_go_helper.mli | 4 -- 5 files changed, 63 insertions(+), 58 deletions(-) diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index 12ef3420d31..2c29a3cbd91 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -4,6 +4,8 @@ open Printf open Datamodel_types +open Datamodel_utils +open Dm_api exception Unknown_wire_protocol @@ -328,3 +330,28 @@ let json_releases = , `Float (float_of_int (List.length unique_version_bumps)) ) ] + +let session_id = + { + param_type= Ref Datamodel_common._session + ; param_name= "session_id" + ; param_doc= "Reference to a valid session" + ; param_release= Datamodel_common.rio_release + ; param_default= None + } + +let objects = + let api = Datamodel.all_api in + (* Add all implicit messages *) + let api = add_implicit_messages api in + (* Only include messages that are visible to a XenAPI client *) + let api = filter (fun _ -> true) (fun _ -> true) on_client_side api in + (* And only messages marked as not hidden from the docs, and non-internal fields *) + let api = + filter + (fun _ -> true) + (fun f -> not f.internal_only) + (fun m -> not m.msg_hide_from_docs) + api + in + objects_of_api api diff --git a/ocaml/sdk-gen/common/CommonFunctions.mli b/ocaml/sdk-gen/common/CommonFunctions.mli index 9a88b5cd5bd..197dcb8a3a4 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.mli +++ b/ocaml/sdk-gen/common/CommonFunctions.mli @@ -1,3 +1,5 @@ +open Datamodel_types + (** Exception for unknown wire protocol. *) exception Unknown_wire_protocol @@ -129,3 +131,9 @@ val render_file : string * string -> Mustache.Json.t -> string -> string -> unit val json_releases : Mustache.Json.t (** JSON structure representing release information. *) + +val session_id : param +(** Param of session_id. *) + +val objects : obj list +(** Objects of api that generate SDKs. *) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index bebd0ef9513..c5fe7988252 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -11,6 +11,7 @@ GNU Lesser General Public License for more details. *) +open CommonFunctions open Gen_go_helper let render_api_messages_and_errors () = diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 6946a4c8436..9414102a002 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -14,8 +14,6 @@ (* Generator of Go bindings from the datamodel *) open Datamodel_types -open Datamodel_utils -open Dm_api open CommonFunctions let dest_dir = "autogen" @@ -165,15 +163,6 @@ module Json = struct ) StringMap.empty ps - let session_id = - { - param_type= Ref Datamodel_common._session - ; param_name= "session_id" - ; param_doc= "Reference to a valid session" - ; param_release= Datamodel_common.rio_release - ; param_default= None - } - let enums_in_messages_of_obj obj = List.fold_left (fun enums msg -> @@ -208,6 +197,31 @@ module Json = struct in `O [("import", `Bool true); ("items", `A items)] + let get_event_snapshot name = + if String.lowercase_ascii name = "event" then + [ + `O + [ + ("name", `String "Snapshot") + ; ( "description" + , `String + "The record of the database object that was added, changed or \ + deleted" + ) + ; ("type", `String "RecordInterface") + ] + ] + else + [] + + let get_event_session_value = function + | "event" -> + [("event", `Bool true); ("session", `Null)] + | "session" -> + [("event", `Null); ("session", `Bool true)] + | _ -> + [("event", `Null); ("session", `Null)] + let xenapi objs = let enums_acc = ref StringMap.empty in let erase_existed enums = @@ -229,43 +243,18 @@ module Json = struct in StringMap.fold (fun k v acc -> of_enum k v :: acc) enums [] in - let event_snapshot = - if String.lowercase_ascii obj.name = "event" then - [ - `O - [ - ("name", `String "Snapshot") - ; ( "description" - , `String - "The record of the database object that was added, \ - changed or deleted" - ) - ; ("type", `String "RecordInterface") - ] - ] - else - [] - in let obj_name = snake_to_camel obj.name in let modules = get_modules obj.messages has_time_type in - let event_session_value = function - | "event" -> - [("event", `Bool true); ("session", `Null)] - | "session" -> - [("event", `Null); ("session", `Bool true)] - | _ -> - [("event", `Null); ("session", `Null)] - in let base_assoc_list = [ ("name", `String obj_name) ; ("description", `String (String.trim obj.description)) - ; ("fields", `A (event_snapshot @ fields)) ; ("enums", `A enums) + ; ("fields", `A (get_event_snapshot obj.name @ fields)) ; ("modules", modules) ] in - let assoc_list = event_session_value obj.name @ base_assoc_list in + let assoc_list = get_event_session_value obj.name @ base_assoc_list in (String.lowercase_ascii obj.name, `O assoc_list) ) objs @@ -276,19 +265,3 @@ module Json = struct let api_errors = List.map (fun error -> `O [("name", `String error)]) !Api_errors.errors end - -let objects = - let api = Datamodel.all_api in - (* Add all implicit messages *) - let api = add_implicit_messages api in - (* Only include messages that are visible to a XenAPI client *) - let api = filter (fun _ -> true) (fun _ -> true) on_client_side api in - (* And only messages marked as not hidden from the docs, and non-internal fields *) - let api = - filter - (fun _ -> true) - (fun f -> not f.internal_only) - (fun m -> not m.msg_hide_from_docs) - api - in - objects_of_api api diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index d4ab0d416ef..c761f3f575a 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -11,14 +11,10 @@ GNU Lesser General Public License for more details. *) -open Datamodel_types - val ( // ) : string -> string -> string val snake_to_camel : string -> string -val objects : obj list - val render_template : string -> Mustache.Json.t -> string val generate_file : string -> string -> unit From b98a102ef9dc8becca7975d0090bbcb58dd79a1e Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 10 Apr 2024 14:46:35 +0800 Subject: [PATCH 10/99] CP-48666: use dune rule to get the destination dir for the generated files Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/dune | 3 ++- ocaml/sdk-gen/go/gen_go_binding.ml | 28 +++++++++++++++++++++------- ocaml/sdk-gen/go/gen_go_helper.ml | 8 ++------ ocaml/sdk-gen/go/gen_go_helper.mli | 3 ++- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/ocaml/sdk-gen/go/dune b/ocaml/sdk-gen/go/dune index 00d835053bf..311ff35f4fd 100644 --- a/ocaml/sdk-gen/go/dune +++ b/ocaml/sdk-gen/go/dune @@ -6,6 +6,7 @@ CommonFunctions mustache xapi-datamodel + xapi-stdext-unix gen_go_helper ) ) @@ -26,7 +27,7 @@ (:x gen_go_binding.exe) (source_tree templates) ) - (action (run %{x})) + (action (run %{x} --destdir autogen)) ) (test diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index c5fe7988252..6c8323f267b 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -14,7 +14,7 @@ open CommonFunctions open Gen_go_helper -let render_api_messages_and_errors () = +let render_api_messages_and_errors destdir = let obj = `O [ @@ -30,11 +30,12 @@ let render_api_messages_and_errors () = let messages_rendered = header ^ render_template "APIMessages.mustache" obj ^ "\n" in - generate_file error_rendered "api_errors.go" ; - generate_file messages_rendered "api_messages.go" + generate_file ~rendered:error_rendered ~destdir ~output_file:"api_errors.go" ; + generate_file ~rendered:messages_rendered ~destdir + ~output_file:"api_messages.go" -let main () = - render_api_messages_and_errors () ; +let main destdir = + render_api_messages_and_errors destdir ; let objects = Json.xenapi objects in List.iter (fun (name, obj) -> @@ -43,8 +44,21 @@ let main () = let record_rendered = render_template "Record.mustache" obj in let rendered = header_rendered ^ enums_rendered ^ record_rendered in let output_file = name ^ ".go" in - generate_file rendered output_file + generate_file ~rendered ~destdir ~output_file ) objects -let _ = main () +let _ = + let destdir = ref "." in + Arg.parse + [ + ( "--destdir" + , Arg.Set_string destdir + , "the destination directory for the generated files" + ) + ] + (fun x -> Printf.fprintf stderr "Ignoring unknown parameter: %s\n%!" x) + "Generates Go SDK." ; + let destdir = !destdir // "src" in + Xapi_stdext_unix.Unixext.mkdir_rec destdir 0o755 ; + main destdir diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 9414102a002..ecb1fc826ad 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -16,14 +16,10 @@ open Datamodel_types open CommonFunctions -let dest_dir = "autogen" - let templates_dir = "templates" let ( // ) = Filename.concat -let src_dir = dest_dir // "src" - let snake_to_camel (s : string) : string = Astring.String.cuts ~sep:"_" s |> List.map (fun s -> Astring.String.cuts ~sep:"-" s) @@ -37,8 +33,8 @@ let render_template template_file json = in Mustache.render templ json -let generate_file rendered output_file = - let out_chan = open_out (src_dir // output_file) in +let generate_file ~rendered ~destdir ~output_file = + let out_chan = open_out (destdir // output_file) in Fun.protect (fun () -> output_string out_chan rendered) ~finally:(fun () -> close_out out_chan) diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index c761f3f575a..9c05728b4f6 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -17,7 +17,8 @@ val snake_to_camel : string -> string val render_template : string -> Mustache.Json.t -> string -val generate_file : string -> string -> unit +val generate_file : + rendered:string -> destdir:string -> output_file:string -> unit module Json : sig val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list From a564280fbefb773398d068d0cc50ba09ab388f0a Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Thu, 11 Apr 2024 14:14:30 +0800 Subject: [PATCH 11/99] CP-48666: generate all enums to a file Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 14 ++++-- ocaml/sdk-gen/go/gen_go_helper.ml | 71 ++++++++++++++---------------- ocaml/sdk-gen/go/gen_go_helper.mli | 4 +- ocaml/sdk-gen/go/test_gen_go.ml | 11 ++--- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 6c8323f267b..05a57ba5337 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -14,6 +14,14 @@ open CommonFunctions open Gen_go_helper +let render_enums enums destdir = + let header = + render_template "FileHeader.mustache" (`O [("modules", `Null)]) + in + let enums = render_template "Enum.mustache" enums |> String.trim in + let rendered = header ^ "\n" ^ enums ^ "\n" in + generate_file ~rendered ~destdir ~output_file:"enums.go" + let render_api_messages_and_errors destdir = let obj = `O @@ -36,13 +44,13 @@ let render_api_messages_and_errors destdir = let main destdir = render_api_messages_and_errors destdir ; - let objects = Json.xenapi objects in + let objects, enums = Json.xenapi objects in + render_enums enums destdir ; List.iter (fun (name, obj) -> let header_rendered = render_template "FileHeader.mustache" obj ^ "\n" in - let enums_rendered = render_template "Enum.mustache" obj in let record_rendered = render_template "Record.mustache" obj in - let rendered = header_rendered ^ enums_rendered ^ record_rendered in + let rendered = header_rendered ^ record_rendered in let output_file = name ^ ".go" in generate_file ~rendered ~destdir ~output_file ) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index ecb1fc826ad..0fd6a092cd7 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -39,12 +39,6 @@ let generate_file ~rendered ~destdir ~output_file = (fun () -> output_string out_chan rendered) ~finally:(fun () -> close_out out_chan) -module EnumSet = Set.Make (struct - type t = string * (string * string) list - - let compare (x0, _y0) (x1, _y1) = String.compare x0 x1 -end) - module Json = struct type enum = (string * string) list @@ -219,41 +213,42 @@ module Json = struct [("event", `Null); ("session", `Null)] let xenapi objs = - let enums_acc = ref StringMap.empty in - let erase_existed enums = - let enums = - StringMap.filter (fun k _ -> not (StringMap.mem k !enums_acc)) enums - in - enums_acc := StringMap.union choose_enum !enums_acc enums ; - enums - in - List.map - (fun obj -> - let fields, enums_in_fields = fields_of_obj_with_enums obj in - let enums_in_msgs = enums_in_messages_of_obj obj in - let enums = + let objs, enums = + List.fold_left + (fun (objs_acc, enums_acc) obj -> + let fields, enums_in_fields, has_time_type = + fields_of_obj_with_enums obj + in + let enums_in_msgs = enums_in_messages_of_obj obj in let enums = - enums_in_fields + enums_acc |> StringMap.union choose_enum enums_in_msgs - |> erase_existed + |> StringMap.union choose_enum enums_in_fields in - StringMap.fold (fun k v acc -> of_enum k v :: acc) enums [] - in - let obj_name = snake_to_camel obj.name in - let modules = get_modules obj.messages has_time_type in - let base_assoc_list = - [ - ("name", `String obj_name) - ; ("description", `String (String.trim obj.description)) - ; ("enums", `A enums) - ; ("fields", `A (get_event_snapshot obj.name @ fields)) - ; ("modules", modules) - ] - in - let assoc_list = get_event_session_value obj.name @ base_assoc_list in - (String.lowercase_ascii obj.name, `O assoc_list) - ) - objs + let obj_name = snake_to_camel obj.name in + let modules = get_modules obj.messages has_time_type in + let base_assoc_list = + [ + ("name", `String obj_name) + ; ("description", `String (String.trim obj.description)) + ; ("fields", `A (get_event_snapshot obj.name @ fields)) + ; ("modules", modules) + ] + in + let assoc_list = get_event_session_value obj.name @ base_assoc_list in + ((String.lowercase_ascii obj.name, `O assoc_list) :: objs_acc, enums) + ) + ([], StringMap.empty) objs + in + let enums = + `O + [ + ( "enums" + , `A (StringMap.fold (fun k v acc -> of_enum k v :: acc) enums []) + ) + ] + in + (objs, enums) let api_messages = List.map (fun (msg, _) -> `O [("name", `String msg)]) !Api_messages.msgList diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 9c05728b4f6..5b8c04f9f9a 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -21,7 +21,9 @@ val generate_file : rendered:string -> destdir:string -> output_file:string -> unit module Json : sig - val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list + val xenapi : + Datamodel_types.obj list + -> (string * Mustache.Json.t) list * Mustache.Json.t val api_messages : Mustache.Json.value list diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 7380e93ab60..b089de08c8d 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -280,24 +280,25 @@ module TestGeneratedJson = struct let verify description actual expected = Alcotest.(check @@ generated_json) description expected actual - let json = enums |> merge record |> merge header + let json = merge record header let testing (name, obj) expected () = verify name obj expected let test_case ((name, _) as test_case) expected = (name, `Quick, testing test_case expected) - let errors_and_messages_tests = + let objects, enums_actual = Json.xenapi objects + + let other_tests = let errors = `O [("api_errors", `A Json.api_errors)] in let messages = `O [("api_messages", `A Json.api_messages)] in [ test_case ("api_errors", errors) api_errors ; test_case ("api_messages", messages) api_messages + ; test_case ("enums", enums_actual) enums ] - let tests = - (objects |> Json.xenapi |> List.map (fun obj -> test_case obj json)) - @ errors_and_messages_tests + let tests = List.map (fun obj -> test_case obj json) objects @ other_tests end let tests = From 3563bf8d5e100d3fa6955217adcae221dd5988b7 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Fri, 12 Apr 2024 13:31:44 +0800 Subject: [PATCH 12/99] refactor the way of getting enums Before we got the enums alongside objs, the readability of code is so poor. We separate to get them now. Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 3 +- ocaml/sdk-gen/go/gen_go_helper.ml | 177 ++++++++++------------------- ocaml/sdk-gen/go/gen_go_helper.mli | 6 +- ocaml/sdk-gen/go/test_gen_go.ml | 7 +- 4 files changed, 68 insertions(+), 125 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 05a57ba5337..3b632f96aaa 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -44,8 +44,9 @@ let render_api_messages_and_errors destdir = let main destdir = render_api_messages_and_errors destdir ; - let objects, enums = Json.xenapi objects in + let enums = Json.all_enums objects in render_enums enums destdir ; + let objects = Json.xenapi objects in List.iter (fun (name, obj) -> let header_rendered = render_template "FileHeader.mustache" obj ^ "\n" in diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 0fd6a092cd7..bb4bfc2d72b 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -15,6 +15,7 @@ open Datamodel_types open CommonFunctions +module Types = Datamodel_utils.Types let templates_dir = "templates" @@ -48,6 +49,9 @@ module Json = struct let choose_enum _key a _b = Some a + let merge_maps m maps = + List.fold_left (fun acc map -> StringMap.union choose_enum acc map) m maps + let rec string_of_ty_with_enums ty : string * enums = match ty with | SecretString | String -> @@ -91,18 +95,7 @@ module Json = struct in `O [("name", `String name); ("values", `A (List.map of_value vs))] - let fields_of_obj_with_enums obj = - let rec flatten_contents contents = - List.fold_left - (fun l -> function - | Field f -> - f :: l - | Namespace (_name, contents) -> - flatten_contents contents @ l - ) - [] contents - in - let fields = flatten_contents obj.contents in + let of_field field = let concat_and_convert field = let concated = String.concat "" (List.map snake_to_camel field.full_name) @@ -113,79 +106,42 @@ module Json = struct | _ -> concated in - List.fold_left - (fun (fields, enums, has_time_type) field -> - let is_time_type = - match field.ty with DateTime -> true | _ -> false - in - let ty, e = string_of_ty_with_enums field.ty in - ( `O - [ - ("name", `String (concat_and_convert field)) - ; ("description", `String (String.trim field.field_description)) - ; ("type", `String ty) - ] - :: fields - , StringMap.union choose_enum enums e - , if is_time_type then true else has_time_type - ) - ) - ([], StringMap.empty, false) - fields - - let enums_from_result obj msg = - match msg.msg_result with - | None -> - StringMap.empty - | Some (t, _d) -> - if obj.name = "event" && String.lowercase_ascii msg.msg_name = "from" - then - StringMap.empty - else - let _, enums = string_of_ty_with_enums t in - enums + let ty, _e = string_of_ty_with_enums field.ty in + `O + [ + ("name", `String (concat_and_convert field)) + ; ("description", `String (String.trim field.field_description)) + ; ("type", `String ty) + ] - let enums_from_params ps = - List.fold_left - (fun enums p -> - let _t, e = string_of_ty_with_enums p.param_type in - StringMap.union choose_enum enums e - ) - StringMap.empty ps + let modules_of_type = function + | DateTime -> + [`O [("name", `String "time"); ("sname", `Null)]] + | _ -> + [] - let enums_in_messages_of_obj obj = - List.fold_left - (fun enums msg -> - let params = - if msg.msg_session then - session_id :: msg.msg_params - else - msg.msg_params - in - let enums1 = enums_from_result obj msg in - let enums2 = enums_from_params params in - enums - |> StringMap.union choose_enum enums1 - |> StringMap.union choose_enum enums2 - ) - StringMap.empty obj.messages + let modules_of_types types = + let common = [`O [("name", `String "fmt"); ("sname", `Null)]] in + let items = + List.map modules_of_type types |> List.concat |> List.append common + in + `O [("import", `Bool true); ("items", `A items)] - let get_modules messages has_time_type = - match messages with - | [] -> - `Null - | _ -> - let items = - match has_time_type with - | true -> - [ - `O [("name", `String "fmt"); ("sname", `Null)] - ; `O [("name", `String "time"); ("sname", `Null)] - ] - | false -> - [`O [("name", `String "fmt"); ("sname", `Null)]] - in - `O [("import", `Bool true); ("items", `A items)] + let all_enums objs = + let enums = + Types.of_objects objs + |> List.map (fun ty -> + let _, e = string_of_ty_with_enums ty in + e + ) + |> merge_maps StringMap.empty + in + `O + [ + ( "enums" + , `A (StringMap.fold (fun k v acc -> of_enum k v :: acc) enums []) + ) + ] let get_event_snapshot name = if String.lowercase_ascii name = "event" then @@ -213,42 +169,27 @@ module Json = struct [("event", `Null); ("session", `Null)] let xenapi objs = - let objs, enums = - List.fold_left - (fun (objs_acc, enums_acc) obj -> - let fields, enums_in_fields, has_time_type = - fields_of_obj_with_enums obj - in - let enums_in_msgs = enums_in_messages_of_obj obj in - let enums = - enums_acc - |> StringMap.union choose_enum enums_in_msgs - |> StringMap.union choose_enum enums_in_fields - in - let obj_name = snake_to_camel obj.name in - let modules = get_modules obj.messages has_time_type in - let base_assoc_list = - [ - ("name", `String obj_name) - ; ("description", `String (String.trim obj.description)) - ; ("fields", `A (get_event_snapshot obj.name @ fields)) - ; ("modules", modules) - ] - in - let assoc_list = get_event_session_value obj.name @ base_assoc_list in - ((String.lowercase_ascii obj.name, `O assoc_list) :: objs_acc, enums) - ) - ([], StringMap.empty) objs - in - let enums = - `O - [ - ( "enums" - , `A (StringMap.fold (fun k v acc -> of_enum k v :: acc) enums []) - ) - ] - in - (objs, enums) + List.map + (fun obj -> + let fields = Datamodel_utils.fields_of_obj obj in + let types = List.map (fun field -> field.ty) fields in + let modules = + match obj.messages with [] -> `Null | _ -> modules_of_types types + in + let base_assoc_list = + [ + ("name", `String (snake_to_camel obj.name)) + ; ("description", `String (String.trim obj.description)) + ; ( "fields" + , `A (get_event_snapshot obj.name @ List.map of_field fields) + ) + ; ("modules", modules) + ] + in + let assoc_list = get_event_session_value obj.name @ base_assoc_list in + (String.lowercase_ascii obj.name, `O assoc_list) + ) + objs let api_messages = List.map (fun (msg, _) -> `O [("name", `String msg)]) !Api_messages.msgList diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 5b8c04f9f9a..557659ec0cd 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -21,9 +21,9 @@ val generate_file : rendered:string -> destdir:string -> output_file:string -> unit module Json : sig - val xenapi : - Datamodel_types.obj list - -> (string * Mustache.Json.t) list * Mustache.Json.t + val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list + + val all_enums : Datamodel_types.obj list -> Mustache.Json.t val api_messages : Mustache.Json.value list diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index b089de08c8d..52fcbc85d19 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -287,17 +287,18 @@ module TestGeneratedJson = struct let test_case ((name, _) as test_case) expected = (name, `Quick, testing test_case expected) - let objects, enums_actual = Json.xenapi objects - let other_tests = + let enums_all = Json.all_enums objects in let errors = `O [("api_errors", `A Json.api_errors)] in let messages = `O [("api_messages", `A Json.api_messages)] in [ test_case ("api_errors", errors) api_errors ; test_case ("api_messages", messages) api_messages - ; test_case ("enums", enums_actual) enums + ; test_case ("enums", enums_all) enums ] + let objects = Json.xenapi objects + let tests = List.map (fun obj -> test_case obj json) objects @ other_tests end From f9cb49910e7e89c03d79b316998f5bf9bfa62544 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Thu, 18 Apr 2024 10:47:18 +0800 Subject: [PATCH 13/99] CP-48666: refactor the JSON schema checking Just check the fields only we need instead of generalized recursive functions Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 2 +- ocaml/sdk-gen/go/test_gen_go.ml | 198 +++++++++++++++++++----------- 2 files changed, 130 insertions(+), 70 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index bb4bfc2d72b..0729b3bd2bd 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -186,7 +186,7 @@ module Json = struct ; ("modules", modules) ] in - let assoc_list = get_event_session_value obj.name @ base_assoc_list in + let assoc_list = base_assoc_list @ get_event_session_value obj.name in (String.lowercase_ascii obj.name, `O assoc_list) ) objs diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 52fcbc85d19..76d9bc57bd6 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -42,59 +42,132 @@ module SnakeToCamelTest = Generic.MakeStateless (struct ] end) -let rec is_same_struct_of_value (value1 : Mustache.Json.value) - (value2 : Mustache.Json.value) = - match (value1, value2) with - | `Null, _ | _, `Null -> +let schema_check keys checker members = + let compare_keys lst1 lst2 = + let sorted_lst1 = List.sort String.compare lst1 in + let sorted_lst2 = List.sort String.compare lst2 in + List.compare String.compare sorted_lst1 sorted_lst2 = 0 + in + let keys' = List.map (fun (k, _) -> k) members in + compare_keys keys keys' && List.for_all checker members + +(* field *) +let verify_field_member = function + | "name", `String _ | "description", `String _ | "type", `String _ -> true - | `Bool _, `Bool _ -> + | _ -> + false + +let field_keys = ["name"; "description"; "type"] + +let verify_field = function + | `O members -> + schema_check field_keys verify_field_member members + | _ -> + false + +(* module *) +let verify_module_member = function + | "name", `String _ -> true - | `String _, `String _ -> + | "sname", `Null -> true - | `Float _, `Float _ -> + | _ -> + false + +let module_keys = ["name"; "sname"] + +let verify_modules_item = function + | `O members -> + schema_check module_keys verify_module_member members + | _ -> + false + +(* modules *) +let modules_keys = ["import"; "items"] + +let verify_modules_member = function + | "import", `Bool _ -> true - | `O o1, `O o2 -> - let keys1 = List.sort compare (List.map fst o1) in - let keys2 = List.sort compare (List.map fst o2) in - if keys1 <> keys2 then - false - else - List.for_all - (fun key -> - is_same_struct_of_value (List.assoc key o1) (List.assoc key o2) - ) - keys1 - | `A [], `A [] -> + | "items", `A items -> + List.for_all verify_modules_item items + | _ -> + false + +let enum_values_keys = ["value"; "doc"; "name"; "type"] + +(* enums *) +let verify_enum_values_member = function + | "value", `String _ + | "doc", `String _ + | "name", `String _ + | "type", `String _ -> true - | `A [], `A (x :: xs) -> - List.for_all (fun obj -> is_same_struct_of_value x obj) xs - | `A (x :: xs), `A ys -> - List.for_all (fun obj -> is_same_struct_of_value x obj) (xs @ ys) | _ -> false -let is_same_struct (obj1 : Mustache.Json.t) (obj2 : Mustache.Json.t) = - match (obj1, obj2) with - | `O o1, `O o2 -> - let keys1 = List.sort compare (List.map fst o1) in - let keys2 = List.sort compare (List.map fst o2) in - if keys1 <> keys2 then - false - else - List.for_all - (fun key -> - is_same_struct_of_value (List.assoc key o1) (List.assoc key o2) - ) - keys1 - | `A [], `A [] -> +let verify_enum_values : Mustache.Json.value -> bool = function + | `O values -> + schema_check enum_values_keys verify_enum_values_member values + | _ -> + false + +let enum_keys = ["name"; "values"] + +let verify_enum_content : string * Mustache.Json.value -> bool = function + | "name", `String _ -> true - | `A [], `A (x :: xs) -> - List.for_all (fun obj -> is_same_struct_of_value x obj) xs - | `A (x :: xs), `A ys -> - List.for_all (fun obj -> is_same_struct_of_value x obj) (xs @ ys) + | "values", `A values -> + List.for_all verify_enum_values values | _ -> false +let verify_enum : Mustache.Json.value -> bool = function + | `O values -> + schema_check enum_keys verify_enum_content values + | _ -> + false + +let verify_enums : Mustache.Json.t -> bool = function + | `O [("enums", `A enums)] -> + List.for_all verify_enum enums + | _ -> + false + +(* obj *) +let verify_obj_member = function + | "name", `String _ | "description", `String _ -> + true + | "event", `Bool _ | "event", `Null -> + true + | "session", `Bool _ | "session", `Null -> + true + | "fields", `A fields -> + List.for_all verify_field fields + | "modules", `Null -> + true + | "modules", `O members -> + schema_check modules_keys verify_modules_member members + | _ -> + false + +let obj_keys = ["name"; "description"; "fields"; "modules"; "event"; "session"] + +let verify_obj = function + | `O members -> + schema_check obj_keys verify_obj_member members + | _ -> + false + +let verify_msgs_or_errors lst = + let verify_msg_or_error = function + | `O [("name", `String _)] -> + true + | _ -> + false + in + List.for_all verify_msg_or_error lst + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -266,40 +339,27 @@ module TemplatesTest = Generic.MakeStateless (struct end) module TestGeneratedJson = struct - let merge (obj1 : Mustache.Json.t) (obj2 : Mustache.Json.t) = - match (obj1, obj2) with - | `O list1, `O list2 -> - `O (list1 @ list2) - | _ -> - `O [] + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) - let pp_json = Fmt.of_to_string string_of_json + let test_enums () = + let enums = Json.all_enums objects in + verify "enums" verify_enums enums - let generated_json = Alcotest.testable pp_json is_same_struct + let test_obj () = + Json.xenapi objects + |> List.iter (fun (name, obj) -> verify name verify_obj obj) - let verify description actual expected = - Alcotest.(check @@ generated_json) description expected actual + let test_errors_and_msgs () = + verify "errors_and_msgs" verify_msgs_or_errors + (Json.api_errors @ Json.api_messages) - let json = merge record header - - let testing (name, obj) expected () = verify name obj expected - - let test_case ((name, _) as test_case) expected = - (name, `Quick, testing test_case expected) - - let other_tests = - let enums_all = Json.all_enums objects in - let errors = `O [("api_errors", `A Json.api_errors)] in - let messages = `O [("api_messages", `A Json.api_messages)] in + let tests = [ - test_case ("api_errors", errors) api_errors - ; test_case ("api_messages", messages) api_messages - ; test_case ("enums", enums_all) enums + ("enums", `Quick, test_enums) + ; ("objs", `Quick, test_obj) + ; ("errors_and_msgs", `Quick, test_errors_and_msgs) ] - - let objects = Json.xenapi objects - - let tests = List.map (fun obj -> test_case obj json) objects @ other_tests end let tests = From 1351856966d29d8546b4309972f13eeba823a9cd Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Thu, 18 Apr 2024 13:11:15 +0800 Subject: [PATCH 14/99] CP-48666: refactor `render_template` with an optional newline parameter Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 20 ++++++++++++-------- ocaml/sdk-gen/go/gen_go_helper.ml | 5 +++-- ocaml/sdk-gen/go/gen_go_helper.mli | 3 ++- ocaml/sdk-gen/go/test_gen_go.ml | 3 ++- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 3b632f96aaa..985bc671443 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -16,10 +16,12 @@ open Gen_go_helper let render_enums enums destdir = let header = - render_template "FileHeader.mustache" (`O [("modules", `Null)]) + render_template "FileHeader.mustache" + (`O [("modules", `Null)]) + ~newline:true () in - let enums = render_template "Enum.mustache" enums |> String.trim in - let rendered = header ^ "\n" ^ enums ^ "\n" in + let enums = render_template "Enum.mustache" enums () |> String.trim in + let rendered = header ^ enums ^ "\n" in generate_file ~rendered ~destdir ~output_file:"enums.go" let render_api_messages_and_errors destdir = @@ -31,12 +33,12 @@ let render_api_messages_and_errors destdir = ; ("modules", `Null) ] in - let header = render_template "FileHeader.mustache" obj ^ "\n" in + let header = render_template "FileHeader.mustache" obj ~newline:true () in let error_rendered = - header ^ render_template "APIErrors.mustache" obj ^ "\n" + header ^ render_template "APIErrors.mustache" obj ~newline:true () in let messages_rendered = - header ^ render_template "APIMessages.mustache" obj ^ "\n" + header ^ render_template "APIMessages.mustache" obj ~newline:true () in generate_file ~rendered:error_rendered ~destdir ~output_file:"api_errors.go" ; generate_file ~rendered:messages_rendered ~destdir @@ -49,8 +51,10 @@ let main destdir = let objects = Json.xenapi objects in List.iter (fun (name, obj) -> - let header_rendered = render_template "FileHeader.mustache" obj ^ "\n" in - let record_rendered = render_template "Record.mustache" obj in + let header_rendered = + render_template "FileHeader.mustache" obj ~newline:true () + in + let record_rendered = render_template "Record.mustache" obj () in let rendered = header_rendered ^ record_rendered in let output_file = name ^ ".go" in generate_file ~rendered ~destdir ~output_file diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 0729b3bd2bd..5412f0bc564 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -28,11 +28,12 @@ let snake_to_camel (s : string) : string = |> List.map String.capitalize_ascii |> String.concat "" -let render_template template_file json = +let render_template template_file json ?(newline = false) () = let templ = string_of_file (templates_dir // template_file) |> Mustache.of_string in - Mustache.render templ json + let renndered = Mustache.render templ json in + if newline then renndered ^ "\n" else renndered let generate_file ~rendered ~destdir ~output_file = let out_chan = open_out (destdir // output_file) in diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 557659ec0cd..f6c7643e071 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -15,7 +15,8 @@ val ( // ) : string -> string -> string val snake_to_camel : string -> string -val render_template : string -> Mustache.Json.t -> string +val render_template : + string -> Mustache.Json.t -> ?newline:bool -> unit -> string val generate_file : rendered:string -> destdir:string -> output_file:string -> unit diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 76d9bc57bd6..37a6e037d63 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -315,7 +315,8 @@ module TemplatesTest = Generic.MakeStateless (struct let string_of_output_t = Test_printers.string end - let transform (template, json) = render_template template json |> String.trim + let transform (template, json) = + render_template template json () |> String.trim let file_header_rendered = string_of_file "file_header.go" From 259b1e0efa876d11954b987e9255de91b9aa2d88 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 13:56:15 +0800 Subject: [PATCH 15/99] CP-47361: generate mustache template for deserialize and serialize functions Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- .../go/templates/ConvertBatch.mustache | 20 +++++++ .../sdk-gen/go/templates/ConvertEnum.mustache | 25 +++++++++ .../go/templates/ConvertFloat.mustache | 38 ++++++++++++++ .../sdk-gen/go/templates/ConvertInt.mustache | 25 +++++++++ .../go/templates/ConvertInterface.mustache | 8 +++ .../sdk-gen/go/templates/ConvertMap.mustache | 43 +++++++++++++++ .../go/templates/ConvertOption.mustache | 27 ++++++++++ .../go/templates/ConvertRecord.mustache | 52 +++++++++++++++++++ .../sdk-gen/go/templates/ConvertRef.mustache | 18 +++++++ .../sdk-gen/go/templates/ConvertSet.mustache | 35 +++++++++++++ .../go/templates/ConvertSimpleType.mustache | 20 +++++++ .../sdk-gen/go/templates/ConvertTime.mustache | 34 ++++++++++++ 12 files changed, 345 insertions(+) create mode 100644 ocaml/sdk-gen/go/templates/ConvertBatch.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertEnum.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertFloat.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertInt.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertInterface.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertMap.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertOption.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertRecord.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertRef.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertSet.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertTime.mustache diff --git a/ocaml/sdk-gen/go/templates/ConvertBatch.mustache b/ocaml/sdk-gen/go/templates/ConvertBatch.mustache new file mode 100644 index 00000000000..42f05791cb8 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertBatch.mustache @@ -0,0 +1,20 @@ +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (batch {{type}}, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } +{{#elements}} + {{name_internal}}Value, ok := rpcStruct["{{name}}"] + if ok && {{name_internal}}Value != nil { + batch.{{name_exported}}, err = deserialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), {{name_internal}}Value) + if err != nil { + return + } + } +{{/elements}} + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertEnum.mustache b/ocaml/sdk-gen/go/templates/ConvertEnum.mustache new file mode 100644 index 00000000000..85bb1660c24 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertEnum.mustache @@ -0,0 +1,25 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) (string, error) { + _ = context + return string(value), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + strValue, err := deserializeString(context, input) + if err != nil { + return + } + switch strValue { +{{#items}} + case "{{value}}": + value = {{name}} +{{/items}} + default: + err = fmt.Errorf("unable to parse XenAPI response: got value %q for enum %s at %s, but this is not any of the known values", strValue, "{{type}}", context) + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertFloat.mustache b/ocaml/sdk-gen/go/templates/ConvertFloat.mustache new file mode 100644 index 00000000000..89c5910909d --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertFloat.mustache @@ -0,0 +1,38 @@ +{{#serialize}} +//nolint:unparam +func serialize{{func_name_suffix}}(context string, value {{type}}) (interface{}, error) { + _ = context + if math.IsInf(value, 0) { + if math.IsInf(value, 1) { + return "+Inf", nil + } + return "-Inf", nil + } else if math.IsNaN(value) { + return "NaN", nil + } + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.ParseFloat(strValue, 64) + if err != nil { + switch strValue { + case "+Inf": + return math.Inf(1), nil + case "-Inf": + return math.Inf(-1), nil + case "NaN": + return math.NaN(), nil + } + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertInt.mustache b/ocaml/sdk-gen/go/templates/ConvertInt.mustache new file mode 100644 index 00000000000..dbc7cf37c56 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertInt.mustache @@ -0,0 +1,25 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) ({{type}}, error) { + _ = context + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.Atoi(strValue) + if err != nil { + floatValue, err1 := strconv.ParseFloat(strValue, 64) + if err1 == nil { + return int(floatValue), nil + } + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertInterface.mustache b/ocaml/sdk-gen/go/templates/ConvertInterface.mustache new file mode 100644 index 00000000000..9090d083058 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertInterface.mustache @@ -0,0 +1,8 @@ +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (inter {{type}}, err error) { + _ = context + inter = input + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertMap.mustache b/ocaml/sdk-gen/go/templates/ConvertMap.mustache new file mode 100644 index 00000000000..b4cca1d7ca8 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertMap.mustache @@ -0,0 +1,43 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, goMap {{type}}) (xenMap map[string]interface{}, err error) { + xenMap = make(map[string]interface{}) + for goKey, goValue := range goMap { + keyContext := fmt.Sprintf("%s[%s]", context, goKey) + xenKey, err := serialize{{key_type}}(keyContext, goKey) + if err != nil { + return xenMap, err + } + xenValue, err := serialize{{value_type}}(keyContext, goValue) + if err != nil { + return xenMap, err + } + xenMap[xenKey] = xenValue + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (goMap {{type}}, err error) { + xenMap, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + goMap = make({{type}}, len(xenMap)) + for xenKey, xenValue := range xenMap { + keyContext := fmt.Sprintf("%s[%s]", context, xenKey) + goKey, err := deserialize{{key_type}}(keyContext, xenKey) + if err != nil { + return goMap, err + } + goValue, err := deserialize{{value_type}}(keyContext, xenValue) + if err != nil { + return goMap, err + } + goMap[goKey] = goValue + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertOption.mustache b/ocaml/sdk-gen/go/templates/ConvertOption.mustache new file mode 100644 index 00000000000..ca49dea9f3b --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertOption.mustache @@ -0,0 +1,27 @@ +{{#serialize}} +func serializeOption{{func_name_suffix}}(context string, input Option{{func_name_suffix}}) (option interface{}, err error) { + if input == nil { + return + } + option, err = serialize{{func_name_suffix}}(context, *input) + if err != nil { + return + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserializeOption{{func_name_suffix}}(context string, input interface{}) (option Option{{func_name_suffix}}, err error) { + if input == nil { + return + } + value, err := deserialize{{func_name_suffix}}(context, input) + if err != nil { + return + } + option = &value + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertRecord.mustache b/ocaml/sdk-gen/go/templates/ConvertRecord.mustache new file mode 100644 index 00000000000..6e973b87dd0 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertRecord.mustache @@ -0,0 +1,52 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, record {{type}}) (rpcStruct map[string]interface{}, err error) { + rpcStruct = map[string]interface{}{} +{{#fields}} +{{#type_option}} + {{name_internal}}, err := serializeOption{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), record.{{name_exported}}) + if err != nil { + return + } + if {{name_internal}} != nil { + rpcStruct["{{name}}"] = {{name_internal}} + } +{{/type_option}} +{{^type_option}} + rpcStruct["{{name}}"], err = serialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), record.{{name_exported}}) + if err != nil { + return + } +{{/type_option}} +{{/fields}} + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (record {{type}}, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } +{{#fields}} +{{#type_option}} + record.{{name_exported}}, err = deserializeOption{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), rpcStruct["{{name}}"]) + if err != nil { + return + } +{{/type_option}} +{{^type_option}} + {{name_internal}}Value, ok := rpcStruct["{{name}}"] + if ok && {{name_internal}}Value != nil { + record.{{name_exported}}, err = deserialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), {{name_internal}}Value) + if err != nil { + return + } + } +{{/type_option}} +{{/fields}} + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertRef.mustache b/ocaml/sdk-gen/go/templates/ConvertRef.mustache new file mode 100644 index 00000000000..2b938dcb658 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertRef.mustache @@ -0,0 +1,18 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, ref {{type}}) (string, error) { + _ = context + return string(ref), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) ({{type}}, error) { + var ref {{type}} + value, ok := input.(string) + if !ok { + return ref, fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return {{type}}(value), nil +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertSet.mustache b/ocaml/sdk-gen/go/templates/ConvertSet.mustache new file mode 100644 index 00000000000..c3f37099a78 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertSet.mustache @@ -0,0 +1,35 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, slice []{{type}}) (set []interface{}, err error) { + set = make([]interface{}, len(slice)) + for index, item := range slice { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := serialize{{item_func_suffix}}(itemContext, item) + if err != nil { + return set, err + } + set[index] = itemValue + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (slice []{{type}}, err error) { + set, ok := input.([]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "[]interface{}", context, reflect.TypeOf(input), input) + return + } + slice = make([]{{type}}, len(set)) + for index, item := range set { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := deserialize{{item_func_suffix}}(itemContext, item) + if err != nil { + return slice, err + } + slice[index] = itemValue + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache b/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache new file mode 100644 index 00000000000..552052932a6 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache @@ -0,0 +1,20 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) ({{type}}, error) { + _ = context + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + if input == nil { + return + } + value, ok := input.({{type}}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "{{type}}", context, reflect.TypeOf(input), input) + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertTime.mustache b/ocaml/sdk-gen/go/templates/ConvertTime.mustache new file mode 100644 index 00000000000..d79f65841ad --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertTime.mustache @@ -0,0 +1,34 @@ +{{#serialize}} +var timeFormats = []string{time.RFC3339, "20060102T15:04:05Z", "20060102T15:04:05"} + +//nolint:unparam +func serialize{{func_name_suffix}}(context string, value {{type}}) (string, error) { + _ = context + return value.Format(time.RFC3339), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + floatValue, err := strconv.ParseFloat(strValue, 64) + if err != nil { + for _, timeFormat := range timeFormats { + value, err = time.Parse(timeFormat, strValue) + if err == nil { + return value, nil + } + } + return + } + unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) + value = time.Unix(unixTimestamp, 0) + + return +} + +{{/deserialize}} \ No newline at end of file From e6c911caf89afa00099d288d79cdafbd216a27d1 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 14:20:01 +0800 Subject: [PATCH 16/99] CP-47358: Generate convert functions Go code Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 53 ++++ ocaml/sdk-gen/common/CommonFunctions.mli | 8 + ocaml/sdk-gen/go/gen_go_binding.ml | 49 ++++ ocaml/sdk-gen/go/gen_go_helper.ml | 305 ++++++++++++++++++++++- ocaml/sdk-gen/go/gen_go_helper.mli | 69 +++++ 5 files changed, 482 insertions(+), 2 deletions(-) diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index 2c29a3cbd91..989448f49e2 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -355,3 +355,56 @@ let objects = api in objects_of_api api + +module TypesOfMessages = struct + open Xapi_stdext_std + + let records = + List.map + (fun obj -> + let obj_name = String.lowercase_ascii obj.name in + (obj_name, Datamodel_utils.fields_of_obj obj) + ) + objects + + let rec decompose = function + | Set x as y -> + y :: decompose x + | Map (a, b) as y -> + (y :: decompose a) @ decompose b + | Option x as y -> + y :: decompose x + | Record r as y -> + let name = String.lowercase_ascii r in + let types_in_field = + List.assoc_opt name records + |> Option.value ~default:[] + |> List.concat_map (fun field -> decompose field.ty) + in + y :: types_in_field + | (SecretString | String | Int | Float | DateTime | Enum _ | Bool | Ref _) + as x -> + [x] + + let mesages objects = objects |> List.concat_map (fun x -> x.messages) + + (** All types of params in a list of objects (automatically decomposes) *) + let of_params objects = + let param_types = + mesages objects + |> List.concat_map (fun x -> x.msg_params) + |> List.map (fun p -> p.param_type) + |> Listext.List.setify + in + List.concat_map decompose param_types |> Listext.List.setify + + (** All types of results in a list of objects (automatically decomposes) *) + let of_results objects = + let return_types = + let aux accu msg = + match msg.msg_result with None -> accu | Some (ty, _) -> ty :: accu + in + mesages objects |> List.fold_left aux [] |> Listext.List.setify + in + List.concat_map decompose return_types |> Listext.List.setify +end diff --git a/ocaml/sdk-gen/common/CommonFunctions.mli b/ocaml/sdk-gen/common/CommonFunctions.mli index 197dcb8a3a4..71106ad1960 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.mli +++ b/ocaml/sdk-gen/common/CommonFunctions.mli @@ -137,3 +137,11 @@ val session_id : param val objects : obj list (** Objects of api that generate SDKs. *) + +module TypesOfMessages : sig + val of_params : Datamodel_types.obj list -> Datamodel_types.ty list + (** All the types in the params of messages*) + + val of_results : Datamodel_types.obj list -> Datamodel_types.ty list + (** All the types in the results of messages*) +end diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 985bc671443..7e7a6ad726f 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -44,10 +44,59 @@ let render_api_messages_and_errors destdir = generate_file ~rendered:messages_rendered ~destdir ~output_file:"api_messages.go" +let render_convert_header () = + let name s = `O [("name", `String s); ("sname", `Null)] in + let obj = + `O + [ + ("name", `String "convert") + ; ( "modules" + , `O + [ + ("import", `Bool true) + ; ( "items" + , `A (List.map name ["fmt"; "math"; "reflect"; "strconv"; "time"]) + ) + ] + ) + ] + in + render_template "FileHeader.mustache" obj ~newline:true () + +let render_converts destdir = + let event = render_template "ConvertBatch.mustache" Convert.event_batch () in + let interface = + render_template "ConvertInterface.mustache" Convert.interface () + in + let param_types = TypesOfMessages.of_params objects in + let result_types = TypesOfMessages.of_results objects in + let generate types of_json = + types + |> List.map (fun ty -> + let params = Convert.of_ty ty in + let template = Convert.template_of_convert params in + let json : Mustache.Json.t = of_json params in + render_template template json () + ) + |> String.concat "" + in + let rendered = + let serializes_rendered = generate param_types Convert.of_serialize in + let deserializes_rendered = generate result_types Convert.of_deserialize in + render_convert_header () + ^ serializes_rendered + ^ deserializes_rendered + ^ event + ^ String.trim interface + ^ "\n" + in + generate_file ~rendered ~destdir ~output_file:"convert.go" + let main destdir = render_api_messages_and_errors destdir ; let enums = Json.all_enums objects in render_enums enums destdir ; + render_converts destdir ; let objects = Json.xenapi objects in List.iter (fun (name, obj) -> diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 5412f0bc564..92f2bf97dd9 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -15,7 +15,6 @@ open Datamodel_types open CommonFunctions -module Types = Datamodel_utils.Types let templates_dir = "templates" @@ -28,6 +27,14 @@ let snake_to_camel (s : string) : string = |> List.map String.capitalize_ascii |> String.concat "" +let records = + List.map + (fun obj -> + let obj_name = snake_to_camel obj.name ^ "Record" in + (obj_name, Datamodel_utils.fields_of_obj obj) + ) + objects + let render_template template_file json ?(newline = false) () = let templ = string_of_file (templates_dir // template_file) |> Mustache.of_string @@ -53,6 +60,33 @@ module Json = struct let merge_maps m maps = List.fold_left (fun acc map -> StringMap.union choose_enum acc map) m maps + let rec func_name_suffix ty = + match ty with + | SecretString | String -> + "String" + | Int -> + "Int" + | Float -> + "Float" + | Bool -> + "Bool" + | DateTime -> + "Time" + | Enum (name, _) -> + "Enum" ^ snake_to_camel name + | Set ty -> + func_name_suffix ty ^ "Set" + | Map (ty1, ty2) -> + let k_suffix = func_name_suffix ty1 in + let v_suffix = func_name_suffix ty2 in + k_suffix ^ "To" ^ v_suffix ^ "Map" + | Ref r -> + snake_to_camel r ^ "Ref" + | Record r -> + snake_to_camel r ^ "Record" + | Option ty -> + func_name_suffix ty + let rec string_of_ty_with_enums ty : string * enums = match ty with | SecretString | String -> @@ -130,7 +164,7 @@ module Json = struct let all_enums objs = let enums = - Types.of_objects objs + Datamodel_utils.Types.of_objects objs |> List.map (fun ty -> let _, e = string_of_ty_with_enums ty in e @@ -198,3 +232,270 @@ module Json = struct let api_errors = List.map (fun error -> `O [("name", `String error)]) !Api_errors.errors end + +module Convert = struct + type params = {func_suffix: string; value_ty: string} + + type params_of_option = {func_suffix: string} + + type params_of_set = { + func_suffix: string + ; value_ty: string + ; item_fp_type: string + } + + type params_of_record_field = { + name: string + ; name_internal: string + ; name_exported: string + ; func_suffix: string + ; type_option: bool + } + + type params_of_record = { + func_suffix: string + ; value_ty: string + ; fields: params_of_record_field list + } + + type params_of_enum_item = {value: string; name: string} + + type params_of_enum = { + func_suffix: string + ; value_ty: string + ; items: params_of_enum_item list + } + + type params_of_map = { + func_suffix: string + ; value_ty: string + ; key_ty: string + ; val_ty: string + } + + type convert_params = + | Simple of params + | Int of params + | Float of params + | Time of params + | Ref of params + | Option of params_of_option + | Set of params_of_set + | Enum of params_of_enum + | Record of params_of_record + | Map of params_of_map + + let template_of_convert : convert_params -> string = function + | Simple _ -> + "ConvertSimpleType.mustache" + | Int _ -> + "ConvertInt.mustache" + | Float _ -> + "ConvertFloat.mustache" + | Time _ -> + "ConvertTime.mustache" + | Ref _ -> + "ConvertRef.mustache" + | Set _ -> + "ConvertSet.mustache" + | Record _ -> + "ConvertRecord.mustache" + | Map _ -> + "ConvertMap.mustache" + | Enum _ -> + "ConvertEnum.mustache" + | Option _ -> + "ConvertOption.mustache" + + let to_json : convert_params -> Mustache.Json.value = function + | Simple params | Int params | Float params | Time params | Ref params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ] + | Option params -> + `O [("func_name_suffix", `String params.func_suffix)] + | Set params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("item_func_suffix", `String params.item_fp_type) + ] + | Record params -> + let fields = + List.rev_map + (fun (field : params_of_record_field) -> + `O + [ + ("name", `String field.name) + ; ("name_internal", `String field.name_internal) + ; ("name_exported", `String field.name_exported) + ; ("func_name_suffix", `String field.func_suffix) + ; ("type_option", `Bool field.type_option) + ] + ) + params.fields + in + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("fields", `A fields) + ] + | Enum params -> + let of_value item = + `O [("value", `String item.value); ("name", `String item.name)] + in + `O + [ + ("type", `String params.value_ty) + ; ("func_name_suffix", `String params.func_suffix) + ; ("items", `A (List.map of_value params.items)) + ] + | Map params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("key_type", `String params.key_ty) + ; ("value_type", `String params.val_ty) + ] + + let fields record_name = + let fields = + List.assoc_opt record_name records + |> Option.value ~default:[] + |> List.rev_map (fun field -> + ( String.concat "_" field.full_name + , Json.func_name_suffix field.ty + , match field.ty with Option _ -> true | _ -> false + ) + ) + in + if record_name = "EventRecord" then + ("snapshot", "RecordInterface", false) :: fields + else + fields + + let of_ty = function + | SecretString | String -> + Simple {func_suffix= "String"; value_ty= "string"} + | Int -> + Int {func_suffix= "Int"; value_ty= "int"} + | Float -> + Float {func_suffix= "Float"; value_ty= "float64"} + | Bool -> + Simple {func_suffix= "Bool"; value_ty= "bool"} + | DateTime -> + Time {func_suffix= "Time"; value_ty= "time.Time"} + | Enum (name, kv) as ty -> + let name = snake_to_camel name in + let items = + List.map (fun (k, _) -> {value= k; name= name ^ snake_to_camel k}) kv + in + Enum {func_suffix= Json.func_name_suffix ty; value_ty= name; items} + | Set ty as set -> + let fp_ty = Json.func_name_suffix ty in + let ty, _ = Json.string_of_ty_with_enums ty in + Set + { + func_suffix= Json.func_name_suffix set + ; value_ty= ty + ; item_fp_type= fp_ty + } + | Map (ty1, ty2) as ty -> + let name, _ = Json.string_of_ty_with_enums ty in + Map + { + func_suffix= Json.func_name_suffix ty + ; value_ty= name + ; key_ty= Json.func_name_suffix ty1 + ; val_ty= Json.func_name_suffix ty2 + } + | Ref _ as ty -> + let name = Json.func_name_suffix ty in + Ref {func_suffix= name; value_ty= name} + | Record r -> + let name = snake_to_camel r ^ "Record" in + let fields = + List.map + (fun (name, func_suffix, is_option_type) -> + let camel_name = snake_to_camel name in + { + name + ; name_internal= String.uncapitalize_ascii camel_name + ; name_exported= camel_name + ; func_suffix + ; type_option= is_option_type + } + ) + (fields name) + in + Record {func_suffix= name; value_ty= name; fields} + | Option ty -> + Option {func_suffix= Json.func_name_suffix ty} + + let of_serialize params = + `O [("serialize", `A [to_json params]); ("deserialize", `Null)] + + let of_deserialize params = + `O [("serialize", `Null); ("deserialize", `A [to_json params])] + + let event_batch : Mustache.Json.t = + `O + [ + ( "deserialize" + , `A + [ + `O + [ + ("func_name_suffix", `String "EventBatch") + ; ("type", `String "EventBatch") + ; ( "elements" + , `A + [ + `O + [ + ("name", `String "token") + ; ("name_internal", `String "token") + ; ("name_exported", `String "Token") + ; ("func_name_suffix", `String "String") + ] + ; `O + [ + ("name", `String "validRefCounts") + ; ("name_internal", `String "validRefCounts") + ; ("name_exported", `String "ValidRefCounts") + ; ("func_name_suffix", `String "StringToIntMap") + ] + ; `O + [ + ("name", `String "events") + ; ("name_internal", `String "events") + ; ("name_exported", `String "Events") + ; ("func_name_suffix", `String "EventRecordSet") + ] + ] + ) + ] + ] + ) + ] + + let interface : Mustache.Json.t = + `O + [ + ( "deserialize" + , `A + [ + `O + [ + ("func_name_suffix", `String "RecordInterface") + ; ("type", `String "RecordInterface") + ] + ] + ) + ] +end diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index f6c7643e071..29a301b965a 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -30,3 +30,72 @@ module Json : sig val api_errors : Mustache.Json.value list end + +module Convert : sig + type params = {func_suffix: string; value_ty: string} + + type params_of_option = {func_suffix: string} + + type params_of_set = { + func_suffix: string + ; value_ty: string + ; item_fp_type: string + } + + type params_of_record_field = { + name: string + ; name_internal: string + ; name_exported: string + ; func_suffix: string + ; type_option: bool + } + + type params_of_record = { + func_suffix: string + ; value_ty: string + ; fields: params_of_record_field list + } + + type params_of_enum_item = {value: string; name: string} + + type params_of_enum = { + func_suffix: string + ; value_ty: string + ; items: params_of_enum_item list + } + + type params_of_map = { + func_suffix: string + ; value_ty: string + ; key_ty: string + ; val_ty: string + } + + type convert_params = + | Simple of params + | Int of params + | Float of params + | Time of params + | Ref of params + | Option of params_of_option + | Set of params_of_set + | Enum of params_of_enum + | Record of params_of_record + | Map of params_of_map + + val template_of_convert : convert_params -> string + + val to_json : convert_params -> Mustache.Json.value + + val fields : string -> (string * string * bool) list + + val of_ty : Datamodel_types.ty -> convert_params + + val of_serialize : convert_params -> Mustache.Json.t + + val of_deserialize : convert_params -> Mustache.Json.t + + val event_batch : Mustache.Json.t + + val interface : Mustache.Json.t +end From c18a5f3c17d4e2eb7d86131be6193a328ab52d3c Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:35:12 +0800 Subject: [PATCH 17/99] CP-48855: update templates (APIErrors, APIMessages, Record) Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/templates/APIErrors.mustache | 3 ++- .../sdk-gen/go/templates/APIMessages.mustache | 2 +- ocaml/sdk-gen/go/templates/Record.mustache | 18 ++++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ocaml/sdk-gen/go/templates/APIErrors.mustache b/ocaml/sdk-gen/go/templates/APIErrors.mustache index 8128b5ef185..9bce9a085dc 100644 --- a/ocaml/sdk-gen/go/templates/APIErrors.mustache +++ b/ocaml/sdk-gen/go/templates/APIErrors.mustache @@ -1,6 +1,7 @@ +//nolint:gosec const ( {{#api_errors}} // - ERR_{{name}} = "{{name}}" + Error{{name}} = "{{value}}" {{/api_errors}} ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/APIMessages.mustache b/ocaml/sdk-gen/go/templates/APIMessages.mustache index 172e4e416c4..3bac101ff6f 100644 --- a/ocaml/sdk-gen/go/templates/APIMessages.mustache +++ b/ocaml/sdk-gen/go/templates/APIMessages.mustache @@ -1,6 +1,6 @@ const ( {{#api_messages}} // - MSG_{{name}} = "{{name}}" + Message{{name}} = "{{value}}" {{/api_messages}} ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/Record.mustache b/ocaml/sdk-gen/go/templates/Record.mustache index e4d874ba978..b30e234e1cb 100644 --- a/ocaml/sdk-gen/go/templates/Record.mustache +++ b/ocaml/sdk-gen/go/templates/Record.mustache @@ -21,21 +21,23 @@ type EventBatch struct { // {{.}} {{/description}} {{#session}} -type {{name}}Class struct { - client *rpcClient - ref SessionRef +type {{name}} struct { + APIVersion APIVersion + client *rpcClient + ref SessionRef + XAPIVersion string } -func NewSession(opts *ClientOpts) *SessionClass { - client := NewJsonRPCClient(opts) - var session SessionClass +func NewSession(opts *ClientOpts) *Session { + client := newJSONRPCClient(opts) + var session Session session.client = client return &session } {{/session}} {{^session}} -type {{name}}Class struct{} +type {{name_internal}} struct{} -var {{name}} *{{name}}Class +var {{name}} {{name_internal}} {{/session}} \ No newline at end of file From 12fcb25f8115e9b7f65ea4adfd50ec58f589a304 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:44:57 +0800 Subject: [PATCH 18/99] CP-48855: adjust generated json for templates changed changed templates: APIVersions.mustache,APIErrors.mustache, Record.mustache Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 34 +++++++++++++-- ocaml/sdk-gen/go/test_data/api_errors.go | 7 ++-- ocaml/sdk-gen/go/test_data/api_messages.go | 4 +- ocaml/sdk-gen/go/test_data/record.go | 14 ++++--- ocaml/sdk-gen/go/test_gen_go.ml | 49 ++++++++++++++++++---- 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 5412f0bc564..fe7194adf65 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -172,6 +172,8 @@ module Json = struct let xenapi objs = List.map (fun obj -> + let obj_name = snake_to_camel obj.name in + let name_internal = String.uncapitalize_ascii obj_name in let fields = Datamodel_utils.fields_of_obj obj in let types = List.map (fun field -> field.ty) fields in let modules = @@ -179,7 +181,8 @@ module Json = struct in let base_assoc_list = [ - ("name", `String (snake_to_camel obj.name)) + ("name", `String obj_name) + ; ("name_internal", `String name_internal) ; ("description", `String (String.trim obj.description)) ; ( "fields" , `A (get_event_snapshot obj.name @ List.map of_field fields) @@ -192,9 +195,32 @@ module Json = struct ) objs + let of_api_message_or_error info = + let snake_to_camel (s : string) : string = + String.split_on_char '_' s + |> List.map (fun seg -> + let lower = String.lowercase_ascii seg in + match lower with + | "vm" + | "cpu" + | "tls" + | "xml" + | "url" + | "id" + | "uuid" + | "ip" + | "api" + | "eof" -> + String.uppercase_ascii lower + | _ -> + String.capitalize_ascii lower + ) + |> String.concat "" + in + `O [("name", `String (snake_to_camel info)); ("value", `String info)] + let api_messages = - List.map (fun (msg, _) -> `O [("name", `String msg)]) !Api_messages.msgList + List.map (fun (msg, _) -> of_api_message_or_error msg) !Api_messages.msgList - let api_errors = - List.map (fun error -> `O [("name", `String error)]) !Api_errors.errors + let api_errors = List.map of_api_message_or_error !Api_errors.errors end diff --git a/ocaml/sdk-gen/go/test_data/api_errors.go b/ocaml/sdk-gen/go/test_data/api_errors.go index b14349c1885..1e6d67aba8a 100644 --- a/ocaml/sdk-gen/go/test_data/api_errors.go +++ b/ocaml/sdk-gen/go/test_data/api_errors.go @@ -1,6 +1,7 @@ +//nolint:gosec const ( // - ERR_MESSAGE_DEPRECATED = "MESSAGE_DEPRECATED" + ErrorMessageDeprecated = "MESSAGE_DEPRECATED" // - ERR_MESSAGE_REMOVED = "MESSAGE_REMOVED" -) + ErrorMessageRemoved = "MESSAGE_REMOVED" +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/api_messages.go b/ocaml/sdk-gen/go/test_data/api_messages.go index f91592ec010..7f54389eb67 100644 --- a/ocaml/sdk-gen/go/test_data/api_messages.go +++ b/ocaml/sdk-gen/go/test_data/api_messages.go @@ -1,6 +1,6 @@ const ( // - MSG_HA_STATEFILE_LOST = "HA_STATEFILE_LOST" + MessageHaStatefileLost = "HA_STATEFILE_LOST" // - MSG_METADATA_LUN_HEALTHY = "METADATA_LUN_HEALTHY" + MessageMetadataLunHealthy = "METADATA_LUN_HEALTHY" ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/record.go b/ocaml/sdk-gen/go/test_data/record.go index fd5908e19c0..c07e18b9fb9 100644 --- a/ocaml/sdk-gen/go/test_data/record.go +++ b/ocaml/sdk-gen/go/test_data/record.go @@ -8,14 +8,16 @@ type SessionRecord struct { type SessionRef string // A session -type SessionClass struct { - client *rpcClient - ref SessionRef +type Session struct { + APIVersion APIVersion + client *rpcClient + ref SessionRef + XAPIVersion string } -func NewSession(opts *ClientOpts) *SessionClass { - client := NewJsonRPCClient(opts) - var session SessionClass +func NewSession(opts *ClientOpts) *Session { + client := newJSONRPCClient(opts) + var session Session session.client = client return &session diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 37a6e037d63..c858c27c84f 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -136,7 +136,7 @@ let verify_enums : Mustache.Json.t -> bool = function (* obj *) let verify_obj_member = function - | "name", `String _ | "description", `String _ -> + | "name", `String _ | "description", `String _ | "name_internal", `String _ -> true | "event", `Bool _ | "event", `Null -> true @@ -151,7 +151,16 @@ let verify_obj_member = function | _ -> false -let obj_keys = ["name"; "description"; "fields"; "modules"; "event"; "session"] +let obj_keys = + [ + "name" + ; "name_internal" + ; "description" + ; "fields" + ; "modules" + ; "event" + ; "session" + ] let verify_obj = function | `O members -> @@ -159,10 +168,18 @@ let verify_obj = function | _ -> false +let verify_msg_or_error_member = function + | "name", `String _ | "value", `String _ -> + true + | _ -> + false + +let keys_in_error_or_msg = ["name"; "value"] + let verify_msgs_or_errors lst = let verify_msg_or_error = function - | `O [("name", `String _)] -> - true + | `O members -> + schema_check keys_in_error_or_msg verify_msg_or_error_member members | _ -> false in @@ -285,8 +302,16 @@ let api_errors : Mustache.Json.t = ( "api_errors" , `A [ - `O [("name", `String "MESSAGE_DEPRECATED")] - ; `O [("name", `String "MESSAGE_REMOVED")] + `O + [ + ("name", `String "MessageDeprecated") + ; ("value", `String "MESSAGE_DEPRECATED") + ] + ; `O + [ + ("name", `String "MessageRemoved") + ; ("value", `String "MESSAGE_REMOVED") + ] ] ) ] @@ -297,8 +322,16 @@ let api_messages : Mustache.Json.t = ( "api_messages" , `A [ - `O [("name", `String "HA_STATEFILE_LOST")] - ; `O [("name", `String "METADATA_LUN_HEALTHY")] + `O + [ + ("name", `String "HaStatefileLost") + ; ("value", `String "HA_STATEFILE_LOST") + ] + ; `O + [ + ("name", `String "MetadataLunHealthy") + ; ("value", `String "METADATA_LUN_HEALTHY") + ] ] ) ] From 2c6b09f790a69586a783e8a8c6c9083dc165ec74 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:37:31 +0800 Subject: [PATCH 19/99] CP-48855: add templates for option and APIVersions Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- .../sdk-gen/go/templates/APIVersions.mustache | 103 ++++++++++++++++++ ocaml/sdk-gen/go/templates/Option.mustache | 4 + 2 files changed, 107 insertions(+) create mode 100644 ocaml/sdk-gen/go/templates/APIVersions.mustache create mode 100644 ocaml/sdk-gen/go/templates/Option.mustache diff --git a/ocaml/sdk-gen/go/templates/APIVersions.mustache b/ocaml/sdk-gen/go/templates/APIVersions.mustache new file mode 100644 index 00000000000..6c25e5a7035 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/APIVersions.mustache @@ -0,0 +1,103 @@ +type APIVersion int + +const ( +{{#releases}} + // {{branding}} ({{code_name}}) + APIVersion{{version_major}}_{{version_minor}}{{#first}} APIVersion = iota + 1{{/first}} +{{/releases}} + APIVersionLatest APIVersion = {{latest_version_index}} + APIVersionUnknown APIVersion = 99 +) + +func (v APIVersion) String() string { + switch v { +{{#releases}} + case APIVersion{{version_major}}_{{version_minor}}: + return "{{version_major}}.{{version_minor}}" +{{/releases}} + case APIVersionUnknown: + return "Unknown" + default: + return "Unknown" + } +} + +var APIVersionMap = map[string]APIVersion{ +{{#releases}} + // + "APIVersion{{version_major}}_{{version_minor}}": APIVersion{{version_major}}_{{version_minor}}, +{{/releases}} + // + "APIVersionLatest": APIVersionLatest, + // + "APIVersionUnknown": APIVersionUnknown, +} + +func GetAPIVersion(major int, minor int) APIVersion { + versionName := fmt.Sprintf("APIVersion%d_%d", major, minor) + apiVersion, ok := APIVersionMap[versionName] + if !ok { + apiVersion = APIVersionUnknown + } + + return apiVersion +} + +func getPoolMaster(session *Session) (HostRef, error) { + var master HostRef + poolRefs, err := Pool.GetAll(session) + if err != nil { + return master, err + } + if len(poolRefs) > 0 { + poolRecord, err := Pool.GetRecord(session, poolRefs[0]) + if err != nil { + return master, err + } + return poolRecord.Master, nil + } + return master, errors.New("pool master not found") +} + +func setSessionDetails(session *Session) error { + err := setAPIVersion(session) + if err != nil { + return err + } + err = setXAPIVersion(session) + if err != nil { + return err + } + return nil +} + +func setAPIVersion(session *Session) error { + session.APIVersion = APIVersionUnknown + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + session.APIVersion = GetAPIVersion(hostRecord.APIVersionMajor, hostRecord.APIVersionMinor) + return nil +} + +func setXAPIVersion(session *Session) error { + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + version, ok := hostRecord.SoftwareVersion["xapi"] + if !ok { + return errors.New("xapi version not found") + } + session.XAPIVersion = version + return nil +} diff --git a/ocaml/sdk-gen/go/templates/Option.mustache b/ocaml/sdk-gen/go/templates/Option.mustache new file mode 100644 index 00000000000..5066597f853 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Option.mustache @@ -0,0 +1,4 @@ +{{#option}} +type Option{{type_name_suffix}} *{{type}} + +{{/option}} \ No newline at end of file From 925f2deab55b96ae47268c58b08a2cfb0a330508 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:15:34 +0800 Subject: [PATCH 20/99] CP-48855: render options Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 3 +- ocaml/sdk-gen/go/gen_go_helper.ml | 54 +++++++++++++++++++++++++++--- ocaml/sdk-gen/go/test_gen_go.ml | 35 +++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 985bc671443..cab4c0e7753 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -54,8 +54,9 @@ let main destdir = let header_rendered = render_template "FileHeader.mustache" obj ~newline:true () in + let options_rendered = render_template "Option.mustache" obj () in let record_rendered = render_template "Record.mustache" obj () in - let rendered = header_rendered ^ record_rendered in + let rendered = header_rendered ^ options_rendered ^ record_rendered in let output_file = name ^ ".go" in generate_file ~rendered ~destdir ~output_file ) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index fe7194adf65..02e81943a15 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -42,6 +42,8 @@ let generate_file ~rendered ~destdir ~output_file = ~finally:(fun () -> close_out out_chan) module Json = struct + open Xapi_stdext_std + type enum = (string * string) list module StringMap = Map.Make (String) @@ -53,6 +55,32 @@ module Json = struct let merge_maps m maps = List.fold_left (fun acc map -> StringMap.union choose_enum acc map) m maps + let rec func_name_suffix = function + | SecretString | String -> + "String" + | Int -> + "Int" + | Float -> + "Float" + | Bool -> + "Bool" + | DateTime -> + "Time" + | Enum (name, _) -> + "Enum" ^ snake_to_camel name + | Set ty -> + func_name_suffix ty ^ "Set" + | Map (ty1, ty2) -> + let k_suffix = func_name_suffix ty1 in + let v_suffix = func_name_suffix ty2 in + k_suffix ^ "To" ^ v_suffix ^ "Map" + | Ref r -> + snake_to_camel r ^ "Ref" + | Record r -> + snake_to_camel r ^ "Record" + | Option ty -> + func_name_suffix ty + let rec string_of_ty_with_enums ty : string * enums = match ty with | SecretString | String -> @@ -81,7 +109,9 @@ module Json = struct | Record r -> (snake_to_camel r ^ "Record", StringMap.empty) | Option ty -> - string_of_ty_with_enums ty + let _, e = string_of_ty_with_enums ty in + let name = func_name_suffix ty in + ("Option" ^ name, e) let of_enum name vs = let name = snake_to_camel name in @@ -123,9 +153,7 @@ module Json = struct let modules_of_types types = let common = [`O [("name", `String "fmt"); ("sname", `Null)]] in - let items = - List.map modules_of_type types |> List.concat |> List.append common - in + let items = List.concat_map modules_of_type types |> List.append common in `O [("import", `Bool true); ("items", `A items)] let all_enums objs = @@ -169,13 +197,28 @@ module Json = struct | _ -> [("event", `Null); ("session", `Null)] + let of_option ty = + let name, _ = string_of_ty_with_enums ty in + `O + [ + ("type", `String name) + ; ("type_name_suffix", `String (func_name_suffix ty)) + ] + + let of_options types = + types + |> List.filter_map (function Option ty -> Some ty | _ -> None) + |> List.map of_option + let xenapi objs = List.map (fun obj -> let obj_name = snake_to_camel obj.name in let name_internal = String.uncapitalize_ascii obj_name in let fields = Datamodel_utils.fields_of_obj obj in - let types = List.map (fun field -> field.ty) fields in + let types = + List.map (fun field -> field.ty) fields |> Listext.List.setify + in let modules = match obj.messages with [] -> `Null | _ -> modules_of_types types in @@ -188,6 +231,7 @@ module Json = struct , `A (get_event_snapshot obj.name @ List.map of_field fields) ) ; ("modules", modules) + ; ("option", `A (of_options types)) ] in let assoc_list = base_assoc_list @ get_event_session_value obj.name in diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index c858c27c84f..147f5bf8e16 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -134,6 +134,20 @@ let verify_enums : Mustache.Json.t -> bool = function | _ -> false +let option_keys = ["type"; "type_name_suffix"] + +let verify_option_member = function + | "type", `String _ | "type_name_suffix", `String _ -> + true + | _ -> + false + +let verify_option = function + | `O members -> + schema_check option_keys verify_option_member members + | _ -> + false + (* obj *) let verify_obj_member = function | "name", `String _ | "description", `String _ | "name_internal", `String _ -> @@ -144,6 +158,8 @@ let verify_obj_member = function true | "fields", `A fields -> List.for_all verify_field fields + | "option", `A options -> + List.for_all verify_option options | "modules", `Null -> true | "modules", `O members -> @@ -160,6 +176,7 @@ let obj_keys = ; "modules" ; "event" ; "session" + ; "option" ] let verify_obj = function @@ -336,6 +353,21 @@ let api_messages : Mustache.Json.t = ) ] +let option = + `O + [ + ( "option" + , `A + [ + `O + [ + ("type", `String "string") + ; ("type_name_suffix", `String "String") + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -361,6 +393,8 @@ module TemplatesTest = Generic.MakeStateless (struct let api_messages_rendered = string_of_file "api_messages.go" + let option_rendered = "type OptionString *string" + let tests = `QuickAndAutoDocumented [ @@ -369,6 +403,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("Enum.mustache", enums), enums_rendered) ; (("APIErrors.mustache", api_errors), api_errors_rendered) ; (("APIMessages.mustache", api_messages), api_messages_rendered) + ; (("Option.mustache", option), option_rendered) ] end) From 2f01d79086dc00c570b1e3e8f773e728121fe644 Mon Sep 17 00:00:00 2001 From: xueqingz Date: Mon, 22 Apr 2024 08:30:57 +0000 Subject: [PATCH 21/99] CP-47355, CP-47360: generate mustache template for xapi data module class messages Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/autogen/src/go.mod | 2 +- ocaml/sdk-gen/go/templates/Methods.mustache | 54 ++++++++++++++++ .../go/templates/SessionMethod.mustache | 64 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 ocaml/sdk-gen/go/templates/Methods.mustache create mode 100644 ocaml/sdk-gen/go/templates/SessionMethod.mustache diff --git a/ocaml/sdk-gen/go/autogen/src/go.mod b/ocaml/sdk-gen/go/autogen/src/go.mod index 0d33115cf62..9a8b7f9c133 100644 --- a/ocaml/sdk-gen/go/autogen/src/go.mod +++ b/ocaml/sdk-gen/go/autogen/src/go.mod @@ -1,3 +1,3 @@ module go/xenapi -go 1.22.0 +go 1.22.2 diff --git a/ocaml/sdk-gen/go/templates/Methods.mustache b/ocaml/sdk-gen/go/templates/Methods.mustache new file mode 100644 index 00000000000..32a0a5c8982 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Methods.mustache @@ -0,0 +1,54 @@ +{{#messages}} +// {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func ({{name_internal}}) {{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) ({{#result}}retval {{type}}, {{/result}}err error) { + method := "{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + if err != nil { + return + } +{{/params}} + {{#result}}result, err := {{/result}}{{^result}}_, err = {{/result}}session.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) +{{#result}} + if err != nil { + return + } + retval, err = deserialize{{func_name_suffix}}(method+" -> ", result) +{{/result}} + return +} + +{{#async}} +// Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func ({{name_internal}}) Async{{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) (retval TaskRef, err error) { + method := "Async.{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + if err != nil { + return + } +{{/params}} + result, err := session.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} + +{{/async}} +{{/messages}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/SessionMethod.mustache b/ocaml/sdk-gen/go/templates/SessionMethod.mustache new file mode 100644 index 00000000000..b73fb057379 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/SessionMethod.mustache @@ -0,0 +1,64 @@ +{{#messages}} +// {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func (class *Session) {{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) ({{#result}}retval {{type}}, {{/result}}err error) { + method := "{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + if err != nil { + return + } +{{/params}} + {{#result}}result, err := {{/result}}{{^result}}_, err = {{/result}}class.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) +{{#result}} + if err != nil { + return + } + retval, err = deserialize{{func_name_suffix}}(method+" -> ", result) +{{/result}} +{{#session_login}} + if err != nil { + return + } + class.ref = retval + err = setSessionDetails(class) +{{/session_login}} +{{#session_logout}} + class.ref = "" +{{/session_logout}} + return +} + +{{#async}} +// Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func (class *Session) Async{{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) (retval TaskRef, err error) { + method := "Async.{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + if err != nil { + return + } +{{/params}} + result, err := class.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} + +{{/async}} +{{/messages}} \ No newline at end of file From 0acbe2a1b1bfd251f071f582aca211c56b8114ae Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 24 Apr 2024 09:56:36 +0800 Subject: [PATCH 22/99] CP-47354: Generate messages functions Golang code for all classes Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 15 +- ocaml/sdk-gen/go/gen_go_helper.ml | 173 +++++++++- ocaml/sdk-gen/go/gen_go_helper.mli | 10 + ocaml/sdk-gen/go/test_data/methods.go | 37 +++ ocaml/sdk-gen/go/test_data/session_method.go | 38 +++ ocaml/sdk-gen/go/test_gen_go.ml | 321 ++++++++++++++++++- 6 files changed, 585 insertions(+), 9 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/methods.go create mode 100644 ocaml/sdk-gen/go/test_data/session_method.go diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 985bc671443..31d98fed8db 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -55,7 +55,20 @@ let main destdir = render_template "FileHeader.mustache" obj ~newline:true () in let record_rendered = render_template "Record.mustache" obj () in - let rendered = header_rendered ^ record_rendered in + let methods_rendered = + if name = "session" then + render_template "SessionMethod.mustache" obj () + else + render_template "Methods.mustache" obj () + in + let rendered = + let first_half = header_rendered ^ record_rendered in + match methods_rendered with + | "" -> + first_half + | _ -> + first_half ^ "\n" ^ methods_rendered + in let output_file = name ^ ".go" in generate_file ~rendered ~destdir ~output_file ) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 5412f0bc564..8da6deb6867 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -32,7 +32,7 @@ let render_template template_file json ?(newline = false) () = let templ = string_of_file (templates_dir // template_file) |> Mustache.of_string in - let renndered = Mustache.render templ json in + let renndered = Mustache.render ~strict:true templ json in if newline then renndered ^ "\n" else renndered let generate_file ~rendered ~destdir ~output_file = @@ -53,6 +53,33 @@ module Json = struct let merge_maps m maps = List.fold_left (fun acc map -> StringMap.union choose_enum acc map) m maps + let rec suffix_of_type ty = + match ty with + | SecretString | String -> + "String" + | Int -> + "Int" + | Float -> + "Float" + | Bool -> + "Bool" + | DateTime -> + "Time" + | Enum (name, _) -> + "Enum" ^ snake_to_camel name + | Set ty -> + suffix_of_type ty ^ "Set" + | Map (ty1, ty2) -> + let k_suffix = suffix_of_type ty1 in + let v_suffix = suffix_of_type ty2 in + k_suffix ^ "To" ^ v_suffix ^ "Map" + | Ref r -> + snake_to_camel r ^ "Ref" + | Record r -> + snake_to_camel r ^ "Record" + | Option ty -> + suffix_of_type ty + let rec string_of_ty_with_enums ty : string * enums = match ty with | SecretString | String -> @@ -169,9 +196,149 @@ module Json = struct | _ -> [("event", `Null); ("session", `Null)] + let of_result obj msg = + match msg.msg_result with + | None -> + `Null + | Some (t, _d) -> + if obj.name = "event" && String.lowercase_ascii msg.msg_name = "from" + then + `O + [ + ("type", `String "EventBatch") + ; ("func_name_suffix", `String "EventBatch") + ] + else + let t', _ = string_of_ty_with_enums t in + `O + [ + ("type", `String t') + ; ("func_name_suffix", `String (suffix_of_type t)) + ] + + let of_params params = + let name_internal name = + let name = name |> snake_to_camel |> String.uncapitalize_ascii in + match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name + in + let of_param param = + let suffix_of_type = suffix_of_type param.param_type in + let t, _e = string_of_ty_with_enums param.param_type in + let name = param.param_name in + [ + ("is_session_id", `Bool (name = "session_id")) + ; ("type", `String t) + ; ("name", `String name) + ; ("name_internal", `String (name_internal name)) + ; ("doc", `String param.param_doc) + ; ("func_name_suffix", `String suffix_of_type) + ] + in + (* We use ',' to seprate params in Go function, we should ignore ',' before first param, + for example `func(a type1, b type2)` is wanted rather than `func(, a type1, b type2)`. + *) + let add_first = function + | head :: rest -> + let head = `O (("first", `Bool true) :: of_param head) in + let rest = + List.map + (fun item -> `O (("first", `Bool false) :: of_param item)) + rest + in + head :: rest + | [] -> + [] + in + `A (add_first params) + + let of_error e = `O [("name", `String e.err_name); ("doc", `String e.err_doc)] + + let of_errors = function + | [] -> + `Null + | errors -> + `A (List.map of_error errors) + + let add_session_info class_name method_name = + match (class_name, method_name) with + | "session", "login_with_password" + | "session", "slave_local_login_with_password" -> + [("session_login", `Bool true); ("session_logout", `Bool false)] + | "session", "logout" | "session", "local_logout" -> + [("session_login", `Bool false); ("session_logout", `Bool true)] + | _ -> + [("session_login", `Bool false); ("session_logout", `Bool false)] + + let desc_of_msg msg ctor_fields = + let ctor = + if msg.msg_tag = FromObject Make then + Printf.sprintf " The constructor args are: %s (* = non-optional)." + ctor_fields + else + "" + in + match msg.msg_doc ^ ctor with + | "" -> + `Null + | desc -> + `String (String.trim desc) + + let ctor_fields_of_obj obj = + Datamodel_utils.fields_of_obj obj + |> List.filter (function + | {qualifier= StaticRO | RW; _} -> + true + | _ -> + false + ) + |> List.map (fun f -> + String.concat "_" f.full_name + ^ if f.default_value = None then "*" else "" + ) + |> String.concat ", " + + let messages_of_obj obj = + let ctor_fields = ctor_fields_of_obj obj in + let params_in_msg msg = + if msg.msg_session then + session_id :: msg.msg_params + else + msg.msg_params + in + List.map + (fun msg -> + let params = params_in_msg msg |> of_params in + let base_assoc_list = + [ + ("method_name", `String msg.msg_name) + ; ("class_name", `String obj.name) + ; ("class_name_exported", `String (snake_to_camel obj.name)) + ; ("method_name_exported", `String (snake_to_camel msg.msg_name)) + ; ("description", desc_of_msg msg ctor_fields) + ; ("result", of_result obj msg) + ; ("params", params) + ; ("errors", of_errors msg.msg_errors) + ; ("has_error", `Bool (msg.msg_errors <> [])) + ; ("async", `Bool msg.msg_async) + ] + in + (* Since the param of `session *Session` isn't needed in functions of session object, + we add a special "func_params" field for session object to ignore `session *Session`.*) + if obj.name = "session" then + `O + (("func_params", msg.msg_params |> of_params) + :: (add_session_info obj.name msg.msg_name @ base_assoc_list) + ) + else + `O base_assoc_list + ) + obj.messages + let xenapi objs = List.map (fun obj -> + let obj_name = snake_to_camel obj.name in + let name_internal = String.uncapitalize_ascii obj_name in let fields = Datamodel_utils.fields_of_obj obj in let types = List.map (fun field -> field.ty) fields in let modules = @@ -179,12 +346,14 @@ module Json = struct in let base_assoc_list = [ - ("name", `String (snake_to_camel obj.name)) + ("name", `String obj_name) + ; ("name_internal", `String name_internal) ; ("description", `String (String.trim obj.description)) ; ( "fields" , `A (get_event_snapshot obj.name @ List.map of_field fields) ) ; ("modules", modules) + ; ("messages", `A (messages_of_obj obj)) ] in let assoc_list = base_assoc_list @ get_event_session_value obj.name in diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index f6c7643e071..85dff4ba4d4 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -22,6 +22,16 @@ val generate_file : rendered:string -> destdir:string -> output_file:string -> unit module Json : sig + type enum = (string * string) list + + module StringMap : Map.S with type key = string + + type enums = enum StringMap.t + + val suffix_of_type : Datamodel_types.ty -> string + + val string_of_ty_with_enums : Datamodel_types.ty -> string * enums + val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list val all_enums : Datamodel_types.obj list -> Mustache.Json.t diff --git a/ocaml/sdk-gen/go/test_data/methods.go b/ocaml/sdk-gen/go/test_data/methods.go new file mode 100644 index 00000000000..3e80d71b0f7 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/methods.go @@ -0,0 +1,37 @@ +// GetLog: GetLog Get the host log file +func (host) GetLog(session *Session, host HostRef) (retval string, err error) { + method := "host.get_log" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) + if err != nil { + return + } + hostArg, err := serializeHostRef(fmt.Sprintf("%s(%s)", method, "host"), host) + if err != nil { + return + } + result, err := session.client.sendCall(method, sessionIDArg, hostArg) + if err != nil { + return + } + retval, err = deserializeString(method+" -> ", result) + return +} + +// AsyncGetLog: GetLog Get the host log file +func (host) AsyncGetLog(session *Session, host HostRef) (retval TaskRef, err error) { + method := "Async.host.get_log" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) + if err != nil { + return + } + hostArg, err := serializeHostRef(fmt.Sprintf("%s(%s)", method, "host"), host) + if err != nil { + return + } + result, err := session.client.sendCall(method, sessionIDArg, hostArg) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/session_method.go b/ocaml/sdk-gen/go/test_data/session_method.go new file mode 100644 index 00000000000..b476f4606a8 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/session_method.go @@ -0,0 +1,38 @@ +// LoginWithPassword: Attempt to authenticate the user); returning a session reference if successful +// +// Errors: +// SESSION_AUTHENTICATION_FAILED - The credentials given by the user are incorrect +func (class *Session) LoginWithPassword(uname string, pwd string) (retval SessionRef, err error) { + method := "session.login_with_password" + unameArg, err := serializeString(fmt.Sprintf("%s(%s)", method, "uname"), uname) + if err != nil { + return + } + pwdArg, err := serializeString(fmt.Sprintf("%s(%s)", method, "pwd"), pwd) + if err != nil { + return + } + result, err := class.client.sendCall(method, unameArg, pwdArg) + if err != nil { + return + } + retval, err = deserializeSessionRef(method+" -> ", result) + if err != nil { + return + } + class.ref = retval + err = setSessionDetails(class) + return +} + +// Logout: Logout Log out of a session +func (class *Session) Logout() (err error) { + method := "session.logout" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), class.ref) + if err != nil { + return + } + _, err = class.client.sendCall(method, sessionIDArg) + class.ref = "" + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 37a6e037d63..c57bd1e49b9 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -51,7 +51,6 @@ let schema_check keys checker members = let keys' = List.map (fun (k, _) -> k) members in compare_keys keys keys' && List.for_all checker members -(* field *) let verify_field_member = function | "name", `String _ | "description", `String _ | "type", `String _ -> true @@ -66,7 +65,134 @@ let verify_field = function | _ -> false -(* module *) +let result_keys = ["type"; "func_partial_type"] + +let verify_result_member = function + | "type", `String _ | "func_partial_type", `String _ -> + true + | _ -> + false + +let error_keys = ["name"; "doc"] + +let verify_error_member = function + | "name", `String _ | "doc", `String _ -> + true + | _ -> + false + +let verify_error = function + | `O error -> + schema_check error_keys verify_error_member error + | _ -> + false + +let param_keys = + [ + "is_session_id" + ; "type" + ; "name" + ; "name_internal" + ; "doc" + ; "func_partial_type" + ; "first" + ] + +let verify_param_member = function + | "is_session_id", `Bool _ + | "first", `Bool _ + | "type", `String _ + | "name", `String _ + | "name_internal", `String _ + | "doc", `String _ + | "func_partial_type", `String _ -> + true + | _ -> + false + +let verify_param = function + | `O param -> + schema_check param_keys verify_param_member param + | _ -> + false + +let verify_message_member = function + | "method_name", `String _ + | "class_name", `String _ + | "class_name_exported", `String _ + | "method_name_exported", `String _ -> + true + | "description", `String _ | "description", `Null -> + true + | "result", `Null -> + true + | "result", `O result -> + schema_check result_keys verify_result_member result + | "params", `A params -> + List.for_all verify_param params + | "errors", `A errors -> + List.for_all verify_error errors + | "async", `Bool _ | "has_error", `Bool _ | "errors", `Null -> + true + | _ -> + false + +let verify_sesseion_message_member = function + | "method_name", `String _ + | "class_name", `String _ + | "class_name_exported", `String _ + | "method_name_exported", `String _ -> + true + | "description", `String _ | "description", `Null -> + true + | "result", `Null -> + true + | "result", `O result -> + schema_check result_keys verify_result_member result + | "params", `A params -> + List.for_all verify_param params + | "header_params", `A params -> + List.for_all verify_param params + | "errors", `A errors -> + List.for_all verify_error errors + | "async", `Bool _ + | "has_error", `Bool _ + | "session_login", `Bool _ + | "session_logout", `Bool _ + | "errors", `Null -> + true + | _ -> + false + +let message_keys = + [ + "method_name" + ; "class_name" + ; "class_name_exported" + ; "method_name_exported" + ; "description" + ; "result" + ; "params" + ; "errors" + ; "has_error" + ; "async" + ] + +let session_message_keys = + ["session_login"; "session_logout"; "header_params"] @ message_keys + +let verify_message = function + | `O members -> + let class_name = + List.assoc_opt "class_name" members |> Option.value ~default:`Null + in + if class_name <> `String "session" then + schema_check message_keys verify_message_member members + else + schema_check session_message_keys verify_sesseion_message_member members + | _ -> + false + let verify_module_member = function | "name", `String _ -> true @@ -83,7 +209,6 @@ let verify_modules_item = function | _ -> false -(* modules *) let modules_keys = ["import"; "items"] let verify_modules_member = function @@ -96,7 +221,6 @@ let verify_modules_member = function let enum_values_keys = ["value"; "doc"; "name"; "type"] -(* enums *) let verify_enum_values_member = function | "value", `String _ | "doc", `String _ @@ -136,7 +260,7 @@ let verify_enums : Mustache.Json.t -> bool = function (* obj *) let verify_obj_member = function - | "name", `String _ | "description", `String _ -> + | "name", `String _ | "description", `String _ | "name_internal", `String _ -> true | "event", `Bool _ | "event", `Null -> true @@ -144,6 +268,8 @@ let verify_obj_member = function true | "fields", `A fields -> List.for_all verify_field fields + | "messages", `A messages -> + List.for_all verify_message messages | "modules", `Null -> true | "modules", `O members -> @@ -151,7 +277,17 @@ let verify_obj_member = function | _ -> false -let obj_keys = ["name"; "description"; "fields"; "modules"; "event"; "session"] +let obj_keys = + [ + "name" + ; "description" + ; "name_internal" + ; "fields" + ; "messages" + ; "modules" + ; "event" + ; "session" + ] let verify_obj = function | `O members -> @@ -303,6 +439,173 @@ let api_messages : Mustache.Json.t = ) ] +let session_messages : Mustache.Json.t = + `O + [ + ( "messages" + , `A + [ + `O + [ + ("session_login", `Bool true) + ; ("session_logout", `Bool false) + ; ("class_name", `String "session") + ; ("name_internal", `String "") + ; ("method_name", `String "login_with_password") + ; ("method_name_exported", `String "LoginWithPassword") + ; ( "description" + , `String + "Attempt to authenticate the user); returning a session \ + reference if successful" + ) + ; ("async", `Bool false) + ; ( "header_params" + , `A + [ + `O + [ + ("type", `String "string") + ; ("name", `String "uname") + ; ("name_internal", `String "uname") + ; ("func_partial_type", `String "String") + ; ("first", `Bool true) + ; ("is_session_id", `Bool false) + ] + ; `O + [ + ("type", `String "string") + ; ("name", `String "pwd") + ; ("name_internal", `String "pwd") + ; ("func_name_suffix", `String "String") + ; ("is_session_id", `Bool false) + ] + ] + ) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "string") + ; ("name", `String "uname") + ; ("name_internal", `String "uname") + ; ("func_partial_type", `String "String") + ; ("first", `Bool true) + ; ("is_session_id", `Bool false) + ] + ; `O + [ + ("type", `String "string") + ; ("name", `String "pwd") + ; ("name_internal", `String "pwd") + ; ("func_name_suffix", `String "String") + ; ("is_session_id", `Bool false) + ] + ] + ) + ; ( "result" + , `O + [ + ("type", `String "SessionRef") + ; ("func_partial_type", `String "SessionRef") + ] + ) + ; ("has_error", `Bool true) + ; ( "errors" + , `A + [ + `O + [ + ("name", `String "SESSION_AUTHENTICATION_FAILED") + ; ( "doc" + , `String + "The credentials given by the user are incorrect" + ) + ] + ] + ) + ] + ; `O + [ + ("session_logout", `Bool true) + ; ("session_login", `Bool false) + ; ("class_name", `String "session") + ; ("class_name_exported", `String "Session") + ; ("method_name", `String "logout") + ; ("method_name_exported", `String "Logout") + ; ("description", `String "Logout Log out of a session") + ; ("async", `Bool false) + ; ("func_params", `A []) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "SessionRef") + ; ("name", `String "session_id") + ; ("name_internal", `String "sessionID") + ; ("func_name_suffix", `String "SessionRef") + ; ("is_session_id", `Bool true) + ] + ] + ) + ; ("result", `Null) + ; ("has_error", `Bool false) + ; ("errors", `A []) + ] + ] + ) + ] + +let messages : Mustache.Json.t = + `O + [ + ( "messages" + , `A + [ + `O + [ + ("class_name", `String "host") + ; ("name_internal", `String "host") + ; ("method_name", `String "get_log") + ; ("method_name_exported", `String "GetLog") + ; ("description", `String "GetLog Get the host log file") + ; ("async", `Bool true) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "SessionRef") + ; ("name", `String "session_id") + ; ("name_internal", `String "sessionID") + ; ("func_name_suffix", `String "SessionRef") + ; ("first", `Bool true) + ] + ; `O + [ + ("type", `String "HostRef") + ; ("name", `String "host") + ; ("name_internal", `String "host") + ; ("func_name_suffix", `String "HostRef") + ; ("first", `Bool false) + ] + ] + ) + ; ( "result" + , `O + [ + ("type", `String "string") + ; ("func_partial_type", `String "String") + ] + ) + ; ("has_error", `Bool false) + ; ("errors", `A []) + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -324,6 +627,10 @@ module TemplatesTest = Generic.MakeStateless (struct let enums_rendered = string_of_file "enum.go" + let methods_rendered = string_of_file "methods.go" + + let session_method_rendered = string_of_file "session_method.go" + let api_errors_rendered = string_of_file "api_errors.go" let api_messages_rendered = string_of_file "api_messages.go" @@ -334,6 +641,8 @@ module TemplatesTest = Generic.MakeStateless (struct (("FileHeader.mustache", header), file_header_rendered) ; (("Record.mustache", record), record_rendered) ; (("Enum.mustache", enums), enums_rendered) + ; (("Methods.mustache", messages), methods_rendered) + ; (("SessionMethod.mustache", session_messages), session_method_rendered) ; (("APIErrors.mustache", api_errors), api_errors_rendered) ; (("APIMessages.mustache", api_messages), api_messages_rendered) ] From 644ce45124fd9309821aab5e621450a90fe2980a Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 6 May 2024 20:15:36 +0800 Subject: [PATCH 23/99] CP-47354: add unit tests for `func_name_suffix` and `string_of_ty_with_enums` Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 145 +++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 11 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index c57bd1e49b9..08c165558a6 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -65,10 +65,10 @@ let verify_field = function | _ -> false -let result_keys = ["type"; "func_partial_type"] +let result_keys = ["type"; "func_name_suffix"] let verify_result_member = function - | "type", `String _ | "func_partial_type", `String _ -> + | "type", `String _ | "func_name_suffix", `String _ -> true | _ -> false @@ -94,7 +94,7 @@ let param_keys = ; "name" ; "name_internal" ; "doc" - ; "func_partial_type" + ; "func_name_suffix" ; "first" ] @@ -105,7 +105,7 @@ let verify_param_member = function | "name", `String _ | "name_internal", `String _ | "doc", `String _ - | "func_partial_type", `String _ -> + | "func_name_suffix", `String _ -> true | _ -> false @@ -151,7 +151,7 @@ let verify_sesseion_message_member = function schema_check result_keys verify_result_member result | "params", `A params -> List.for_all verify_param params - | "header_params", `A params -> + | "func_params", `A params -> List.for_all verify_param params | "errors", `A errors -> List.for_all verify_error errors @@ -179,7 +179,7 @@ let message_keys = ] let session_message_keys = - ["session_login"; "session_logout"; "header_params"] @ message_keys + ["session_login"; "session_logout"; "func_params"] @ message_keys let verify_message = function | `O members -> @@ -459,7 +459,7 @@ let session_messages : Mustache.Json.t = reference if successful" ) ; ("async", `Bool false) - ; ( "header_params" + ; ( "func_params" , `A [ `O @@ -467,7 +467,7 @@ let session_messages : Mustache.Json.t = ("type", `String "string") ; ("name", `String "uname") ; ("name_internal", `String "uname") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ; ("first", `Bool true) ; ("is_session_id", `Bool false) ] @@ -489,7 +489,7 @@ let session_messages : Mustache.Json.t = ("type", `String "string") ; ("name", `String "uname") ; ("name_internal", `String "uname") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ; ("first", `Bool true) ; ("is_session_id", `Bool false) ] @@ -507,7 +507,7 @@ let session_messages : Mustache.Json.t = , `O [ ("type", `String "SessionRef") - ; ("func_partial_type", `String "SessionRef") + ; ("func_name_suffix", `String "SessionRef") ] ) ; ("has_error", `Bool true) @@ -580,6 +580,8 @@ let messages : Mustache.Json.t = ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") ; ("func_name_suffix", `String "SessionRef") + ; ("session", `Bool true) + ; ("session_class", `Bool false) ; ("first", `Bool true) ] ; `O @@ -596,7 +598,7 @@ let messages : Mustache.Json.t = , `O [ ("type", `String "string") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ] ) ; ("has_error", `Bool false) @@ -672,10 +674,131 @@ module TestGeneratedJson = struct ] end +module SuffixOfTypeTest = Generic.MakeStateless (struct + open Datamodel_types + + module Io = struct + type input_t = ty + + type output_t = string + + let string_of_input_t = Json.suffix_of_type + + let string_of_output_t = Test_printers.string + end + + let transform = Json.suffix_of_type + + let tests = + `QuickAndAutoDocumented + [ + (SecretString, "String") + ; (String, "String") + ; (Int, "Int") + ; (Float, "Float") + ; (Bool, "Bool") + ; (Enum ("update_sync", [("a", "b"); ("c", "d")]), "EnumUpdateSync") + ; (Set String, "StringSet") + ; (Map (Int, String), "IntToStringMap") + ; (Ref "pool", "PoolRef") + ; (Record "pool", "PoolRecord") + ; (Option String, "String") + ] +end) + +module StringOfTyWithEnumsTest = struct + open Datamodel_types + module StringMap = Json.StringMap + + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) + + let verify_string (ty, enums) = ty = "string" && enums = StringMap.empty + + let test_string () = + let ty, enums = Json.string_of_ty_with_enums String in + verify "String" verify_string (ty, enums) + + let test_secret_string () = + let ty, enums = Json.string_of_ty_with_enums SecretString in + verify "SecretString" verify_string (ty, enums) + + let verify_float (ty, enums) = ty = "float64" && enums = StringMap.empty + + let test_float () = + let ty, enums = Json.string_of_ty_with_enums Float in + verify "Float" verify_float (ty, enums) + + let verify_bool (ty, enums) = ty = "bool" && enums = StringMap.empty + + let test_bool () = + let ty, enums = Json.string_of_ty_with_enums Bool in + verify "bool" verify_bool (ty, enums) + + let verify_datetime (ty, enums) = ty = "time.Time" && enums = StringMap.empty + + let test_datetime () = + let ty, enums = Json.string_of_ty_with_enums DateTime in + verify "datetime" verify_datetime (ty, enums) + + let enum_lst = [("a", "b"); ("c", "d")] + + let verify_enum (ty, enums) = + ty = "UpdateSync" && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_enum () = + let ty, enums = + Json.string_of_ty_with_enums (Enum ("update_sync", enum_lst)) + in + verify "enum" verify_enum (ty, enums) + + let verify_ref (ty, enums) = ty = "PoolRef" && enums = StringMap.empty + + let test_ref () = + let ty, enums = Json.string_of_ty_with_enums (Ref "pool") in + verify "ref" verify_ref (ty, enums) + + let verify_record (ty, enums) = ty = "PoolRecord" && enums = StringMap.empty + + let test_record () = + let ty, enums = Json.string_of_ty_with_enums (Record "pool") in + verify "datetime" verify_record (ty, enums) + + let test_option () = + let ty, enums = Json.string_of_ty_with_enums (Option String) in + verify "datetime" verify_string (ty, enums) + + let verify_map (ty, enums) = + ty = "map[int]UpdateSync" + && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_map () = + let ty, enums = + Json.string_of_ty_with_enums (Map (Int, Enum ("update_sync", enum_lst))) + in + verify "map" verify_map (ty, enums) + + let tests = + [ + ("String", `Quick, test_string) + ; ("SecretString", `Quick, test_secret_string) + ; ("Float", `Quick, test_float) + ; ("Bool", `Quick, test_bool) + ; ("DateTime", `Quick, test_datetime) + ; ("Enum", `Quick, test_enum) + ; ("Ref", `Quick, test_ref) + ; ("Record", `Quick, test_record) + ; ("Option", `Quick, test_option) + ; ("Map", `Quick, test_map) + ] +end + let tests = make_suite "gen_go_binding_" [ ("snake_to_camel", SnakeToCamelTest.tests) + ; ("suffix_of_type", SuffixOfTypeTest.tests) + ; ("string_of_ty_with_enums", StringOfTyWithEnumsTest.tests) ; ("templates", TemplatesTest.tests) ; ("generated_mustache_jsons", TestGeneratedJson.tests) ] From 5aa929bd4c7b74b6ebe403a3b6810a83ac9986d0 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:26:09 +0800 Subject: [PATCH 24/99] CP-48855: render APIVersion Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 32 ++++--- ocaml/sdk-gen/go/gen_go_binding.ml | 25 +++++ ocaml/sdk-gen/go/test_data/api_versions.go | 103 +++++++++++++++++++++ ocaml/sdk-gen/go/test_gen_go.ml | 80 ++++++++++++++++ 4 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/api_versions.go diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index 2c29a3cbd91..a1b613a34b8 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -308,24 +308,32 @@ let json_releases = in try index_rec 0 list with Not_found -> -1 in - let json_of_rel x = + let of_rel x = let y = version_index_of x unique_version_bumps + 1 in - `O - [ - ( "code_name" - , `String (match x.code_name with Some r -> r | None -> "") - ) - ; ("version_major", `Float (float_of_int x.version_major)) - ; ("version_minor", `Float (float_of_int x.version_minor)) - ; ("branding", `String x.branding) - ; ("version_index", `Float (float_of_int y)) - ] + [ + ("code_name", `String (Option.value x.code_name ~default:"")) + ; ("version_major", `Float (float_of_int x.version_major)) + ; ("version_minor", `Float (float_of_int x.version_minor)) + ; ("branding", `String x.branding) + ; ("version_index", `Float (float_of_int y)) + ] + in + let of_rels releases = + match releases with + | [] -> + `A [] + | head :: tail -> + let head' = `O (("first", `Bool true) :: of_rel head) in + let tail' = + List.map (fun rel -> `O (("first", `Bool false) :: of_rel rel)) tail + in + `A (head' :: tail') in `O [ ("API_VERSION_MAJOR", `Float (Int64.to_float Datamodel.api_version_major)) ; ("API_VERSION_MINOR", `Float (Int64.to_float Datamodel.api_version_minor)) - ; ("releases", `A (List.map (fun x -> json_of_rel x) unique_version_bumps)) + ; ("releases", of_rels unique_version_bumps) ; ( "latest_version_index" , `Float (float_of_int (List.length unique_version_bumps)) ) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index cab4c0e7753..2cec36d9a4b 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -24,6 +24,30 @@ let render_enums enums destdir = let rendered = header ^ enums ^ "\n" in generate_file ~rendered ~destdir ~output_file:"enums.go" +let render_api_versions destdir = + let header_json = + let name s = `O [("name", `String s); ("sname", `Null)] in + `O + [ + ( "modules" + , `O + [ + ("import", `Bool true) + ; ("items", `A (List.map name ["errors"; "fmt"])) + ] + ) + ] + in + let rendered = + let header = + render_template "FileHeader.mustache" header_json ~newline:true () + in + header + ^ render_template "APIVersions.mustache" CommonFunctions.json_releases + ~newline:true () + in + generate_file ~rendered ~destdir ~output_file:"api_versions.go" + let render_api_messages_and_errors destdir = let obj = `O @@ -45,6 +69,7 @@ let render_api_messages_and_errors destdir = ~output_file:"api_messages.go" let main destdir = + render_api_versions destdir ; render_api_messages_and_errors destdir ; let enums = Json.all_enums objects in render_enums enums destdir ; diff --git a/ocaml/sdk-gen/go/test_data/api_versions.go b/ocaml/sdk-gen/go/test_data/api_versions.go new file mode 100644 index 00000000000..b82411d5413 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/api_versions.go @@ -0,0 +1,103 @@ +type APIVersion int + +const ( + // XenServer 4.0 (rio) + APIVersion1_1 APIVersion = iota + 1 + // XenServer 4.1 (miami) + APIVersion1_2 + APIVersionLatest APIVersion = 2 + APIVersionUnknown APIVersion = 99 +) + +func (v APIVersion) String() string { + switch v { + case APIVersion1_1: + return "1.1" + case APIVersion1_2: + return "1.2" + case APIVersionUnknown: + return "Unknown" + default: + return "Unknown" + } +} + +var APIVersionMap = map[string]APIVersion{ + // + "APIVersion1_1": APIVersion1_1, + // + "APIVersion1_2": APIVersion1_2, + // + "APIVersionLatest": APIVersionLatest, + // + "APIVersionUnknown": APIVersionUnknown, +} + +func GetAPIVersion(major int, minor int) APIVersion { + versionName := fmt.Sprintf("APIVersion%d_%d", major, minor) + apiVersion, ok := APIVersionMap[versionName] + if !ok { + apiVersion = APIVersionUnknown + } + + return apiVersion +} + +func getPoolMaster(session *Session) (HostRef, error) { + var master HostRef + poolRefs, err := Pool.GetAll(session) + if err != nil { + return master, err + } + if len(poolRefs) > 0 { + poolRecord, err := Pool.GetRecord(session, poolRefs[0]) + if err != nil { + return master, err + } + return poolRecord.Master, nil + } + return master, errors.New("pool master not found") +} + +func setSessionDetails(session *Session) error { + err := setAPIVersion(session) + if err != nil { + return err + } + err = setXAPIVersion(session) + if err != nil { + return err + } + return nil +} + +func setAPIVersion(session *Session) error { + session.APIVersion = APIVersionUnknown + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + session.APIVersion = GetAPIVersion(hostRecord.APIVersionMajor, hostRecord.APIVersionMinor) + return nil +} + +func setXAPIVersion(session *Session) error { + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + version, ok := hostRecord.SoftwareVersion["xapi"] + if !ok { + return errors.New("xapi version not found") + } + session.XAPIVersion = version + return nil +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 147f5bf8e16..178e754c372 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -202,6 +202,53 @@ let verify_msgs_or_errors lst = in List.for_all verify_msg_or_error lst +let verify_release_member = function + | "branding", `String _ | "code_name", `String _ -> + true + | "first", `Bool _ -> + true + | "version_index", `Float _ + | "version_major", `Float _ + | "version_minor", `Float _ -> + true + | _ -> + false + +let release_keys = + [ + "branding" + ; "code_name" + ; "version_major" + ; "version_minor" + ; "first" + ; "version_index" + ] + +let verify_release = function + | `O members -> + schema_check release_keys verify_release_member members + | _ -> + false + +let version_keys = + ["API_VERSION_MAJOR"; "API_VERSION_MINOR"; "latest_version_index"; "releases"] + +let verify_version_member = function + | "latest_version_index", `Float _ + | "API_VERSION_MAJOR", `Float _ + | "API_VERSION_MINOR", `Float _ -> + true + | "releases", `A releases -> + List.for_all verify_release releases + | _ -> + false + +let verify_version = function + | `O members -> + schema_check version_keys verify_version_member members + | _ -> + false + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -353,6 +400,33 @@ let api_messages : Mustache.Json.t = ) ] +let api_versions : Mustache.Json.t = + `O + [ + ("latest_version_index", `Float 2.) + ; ( "releases" + , `A + [ + `O + [ + ("branding", `String "XenServer 4.0") + ; ("code_name", `String "rio") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 1.) + ; ("first", `Bool true) + ] + ; `O + [ + ("branding", `String "XenServer 4.1") + ; ("code_name", `String "miami") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 2.) + ; ("first", `Bool false) + ] + ] + ) + ] + let option = `O [ @@ -393,6 +467,8 @@ module TemplatesTest = Generic.MakeStateless (struct let api_messages_rendered = string_of_file "api_messages.go" + let api_versions_rendered = string_of_file "api_versions.go" + let option_rendered = "type OptionString *string" let tests = @@ -403,6 +479,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("Enum.mustache", enums), enums_rendered) ; (("APIErrors.mustache", api_errors), api_errors_rendered) ; (("APIMessages.mustache", api_messages), api_messages_rendered) + ; (("APIVersions.mustache", api_versions), api_versions_rendered) ; (("Option.mustache", option), option_rendered) ] end) @@ -423,11 +500,14 @@ module TestGeneratedJson = struct verify "errors_and_msgs" verify_msgs_or_errors (Json.api_errors @ Json.api_messages) + let test_versions () = verify "versions" verify_version json_releases + let tests = [ ("enums", `Quick, test_enums) ; ("objs", `Quick, test_obj) ; ("errors_and_msgs", `Quick, test_errors_and_msgs) + ; ("versions", `Quick, test_versions) ] end From e6c5e65300618e6acd1b8c9f219378b61a18c8fd Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 6 May 2024 11:26:09 +0800 Subject: [PATCH 25/99] CP-48855: fix go lint var-naming warnings Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 48 +++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 02e81943a15..d3e7f71e8da 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -16,16 +16,40 @@ open Datamodel_types open CommonFunctions module Types = Datamodel_utils.Types +module StringSet = Set.Make (String) let templates_dir = "templates" let ( // ) = Filename.concat +let acronyms = + [ + "id" + ; "ip" + ; "vm" + ; "api" + ; "uuid" + ; "cpu" + ; "tls" + ; "https" + ; "url" + ; "db" + ; "xml" + ; "eof" + ] + |> StringSet.of_list + +let is_acronym word = StringSet.mem word acronyms + let snake_to_camel (s : string) : string = Astring.String.cuts ~sep:"_" s - |> List.map (fun s -> Astring.String.cuts ~sep:"-" s) - |> List.concat - |> List.map String.capitalize_ascii + |> List.concat_map (fun s -> Astring.String.cuts ~sep:"-" s) + |> List.map (function + | s when is_acronym s -> + String.uppercase_ascii s + | s -> + String.capitalize_ascii s + ) |> String.concat "" let render_template template_file json ?(newline = false) () = @@ -240,28 +264,22 @@ module Json = struct objs let of_api_message_or_error info = - let snake_to_camel (s : string) : string = + let xapi_constants_renaming (s : string) : string = String.split_on_char '_' s |> List.map (fun seg -> let lower = String.lowercase_ascii seg in match lower with - | "vm" - | "cpu" - | "tls" - | "xml" - | "url" - | "id" - | "uuid" - | "ip" - | "api" - | "eof" -> + | s when is_acronym s -> String.uppercase_ascii lower | _ -> String.capitalize_ascii lower ) |> String.concat "" in - `O [("name", `String (snake_to_camel info)); ("value", `String info)] + `O + [ + ("name", `String (xapi_constants_renaming info)); ("value", `String info) + ] let api_messages = List.map (fun (msg, _) -> of_api_message_or_error msg) !Api_messages.msgList From 8c685e19ec4c25926f91cf4b7eceea8f9e1b823e Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 7 May 2024 18:55:38 +0800 Subject: [PATCH 26/99] fix `StringOfTyWithEnumsTest` after merged Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 01be0ce2776..4767b0c05ec 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -901,11 +901,13 @@ module StringOfTyWithEnumsTest = struct let test_record () = let ty, enums = Json.string_of_ty_with_enums (Record "pool") in - verify "datetime" verify_record (ty, enums) + verify "record" verify_record (ty, enums) + + let verify_option (ty, enums) = ty = "OptionString" && enums = StringMap.empty let test_option () = let ty, enums = Json.string_of_ty_with_enums (Option String) in - verify "datetime" verify_string (ty, enums) + verify "option" verify_option (ty, enums) let verify_map (ty, enums) = ty = "map[int]UpdateSync" From 77762f4755169e8a55433a06535f9e5427ec4b4f Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 8 May 2024 10:56:08 +0800 Subject: [PATCH 27/99] CP-48855: it should be only one empty line at end of Go file Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 74ff7950bbb..b12077a7593 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -80,7 +80,9 @@ let main destdir = render_template "FileHeader.mustache" obj ~newline:true () in let options_rendered = render_template "Option.mustache" obj () in - let record_rendered = render_template "Record.mustache" obj () in + let record_rendered = + render_template "Record.mustache" obj () ~newline:true + in let methods_rendered = if name = "session" then render_template "SessionMethod.mustache" obj () @@ -88,15 +90,14 @@ let main destdir = render_template "Methods.mustache" obj () in let rendered = - let first_half = header_rendered ^ options_rendered ^ record_rendered in - match methods_rendered with - | "" -> - first_half - | _ -> - first_half ^ "\n" ^ methods_rendered + let rendered = + [header_rendered; options_rendered; record_rendered; methods_rendered] + |> String.concat "" + |> String.trim + in + rendered ^ "\n" in - let output_file = name ^ ".go" in - generate_file ~rendered ~destdir ~output_file + generate_file ~rendered ~destdir ~output_file:(name ^ ".go") ) objects From becc9e0424c4b8cf4ca06abd6adeb5e4c8b0767e Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 8 May 2024 15:26:34 +0800 Subject: [PATCH 28/99] CP-47356: expose `published_release_for_param` and `compare_versions` for usage of other modules Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 10 ++++------ ocaml/sdk-gen/common/CommonFunctions.mli | 9 +++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index a1b613a34b8..b2db32f3c51 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -110,7 +110,7 @@ let rec lifecycle_matcher milestone lifecycle = else lifecycle_matcher milestone tl -let get_published_release_for_param releases = +let published_release_for_param releases = let filtered = List.filter (fun x -> x <> "closed" && x <> "3.0.3" && x <> "debug") @@ -138,8 +138,8 @@ let get_release_branding codename = let group_params_per_release params = let same_release p1 p2 = compare_versions - (get_published_release_for_param p1.param_release.internal) - (get_published_release_for_param p2.param_release.internal) + (published_release_for_param p1.param_release.internal) + (published_release_for_param p2.param_release.internal) = 0 in let rec groupByRelease acc = function @@ -240,9 +240,7 @@ and get_deprecated_info_message message = and get_published_info_param message param = let msgRelease = get_published_release message.msg_lifecycle.transitions in - let paramRelease = - get_published_release_for_param param.param_release.internal - in + let paramRelease = published_release_for_param param.param_release.internal in if compare_versions paramRelease msgRelease > 0 then sprintf "First published in %s." (get_release_branding paramRelease) else diff --git a/ocaml/sdk-gen/common/CommonFunctions.mli b/ocaml/sdk-gen/common/CommonFunctions.mli index 197dcb8a3a4..c738f73b4a7 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.mli +++ b/ocaml/sdk-gen/common/CommonFunctions.mli @@ -137,3 +137,12 @@ val session_id : param val objects : obj list (** Objects of api that generate SDKs. *) + +val published_release_for_param : string list -> string + +val compare_versions : string -> string -> int +(** Compare two published releases. + @param published release r1. + @param published release r2. + @return the order diff between r1 and r2. +*) From 438c36be0f151a9156c41b8b89836c11d627d94c Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 20:10:07 +0800 Subject: [PATCH 29/99] CP-47358: Add unit tests for generating convert functions Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_data/batch_convert.go | 29 ++ ocaml/sdk-gen/go/test_data/enum_convert.go | 20 + ocaml/sdk-gen/go/test_data/float_convert.go | 33 ++ ocaml/sdk-gen/go/test_data/int_convert.go | 20 + .../sdk-gen/go/test_data/interface_convert.go | 5 + ocaml/sdk-gen/go/test_data/map_convert.go | 38 ++ ocaml/sdk-gen/go/test_data/option_convert.go | 22 + ocaml/sdk-gen/go/test_data/record_convert.go | 35 ++ ocaml/sdk-gen/go/test_data/ref_convert.go | 13 + ocaml/sdk-gen/go/test_data/set_convert.go | 30 ++ .../go/test_data/simple_type_convert.go | 31 ++ ocaml/sdk-gen/go/test_data/time_convert.go | 29 ++ ocaml/sdk-gen/go/test_gen_go.ml | 375 ++++++++++++++++++ 13 files changed, 680 insertions(+) create mode 100644 ocaml/sdk-gen/go/test_data/batch_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/enum_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/float_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/int_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/interface_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/map_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/option_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/record_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/ref_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/set_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/simple_type_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/time_convert.go diff --git a/ocaml/sdk-gen/go/test_data/batch_convert.go b/ocaml/sdk-gen/go/test_data/batch_convert.go new file mode 100644 index 00000000000..fd0e70607f6 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/batch_convert.go @@ -0,0 +1,29 @@ +func deserializeEventBatch(context string, input interface{}) (batch EventBatch, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + tokenValue, ok := rpcStruct["token"] + if ok && tokenValue != nil { + batch.Token, err = deserializeString(fmt.Sprintf("%s.%s", context, "token"), tokenValue) + if err != nil { + return + } + } + validRefCountsValue, ok := rpcStruct["validRefCounts"] + if ok && validRefCountsValue != nil { + batch.ValidRefCounts, err = deserializeStringToIntMap(fmt.Sprintf("%s.%s", context, "validRefCounts"), validRefCountsValue) + if err != nil { + return + } + } + eventsValue, ok := rpcStruct["events"] + if ok && eventsValue != nil { + batch.Events, err = deserializeEventRecordSet(fmt.Sprintf("%s.%s", context, "events"), eventsValue) + if err != nil { + return + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/enum_convert.go b/ocaml/sdk-gen/go/test_data/enum_convert.go new file mode 100644 index 00000000000..40129c0e5ca --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/enum_convert.go @@ -0,0 +1,20 @@ +func serializeEnumTaskStatusType(context string, value TaskStatusType) (string, error) { + _ = context + return string(value), nil +} + +func deserializeEnumTaskStatusType(context string, input interface{}) (value TaskStatusType, err error) { + strValue, err := deserializeString(context, input) + if err != nil { + return + } + switch strValue { + case "pending": + value = TaskStatusTypePending + case "success": + value = TaskStatusTypeSuccess + default: + err = fmt.Errorf("unable to parse XenAPI response: got value %q for enum %s at %s, but this is not any of the known values", strValue, "TaskStatusType", context) + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/float_convert.go b/ocaml/sdk-gen/go/test_data/float_convert.go new file mode 100644 index 00000000000..736ad6f8111 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/float_convert.go @@ -0,0 +1,33 @@ +//nolint:unparam +func serializeFloat(context string, value float64) (interface{}, error) { + _ = context + if math.IsInf(value, 0) { + if math.IsInf(value, 1) { + return "+Inf", nil + } + return "-Inf", nil + } else if math.IsNaN(value) { + return "NaN", nil + } + return value, nil +} + +func deserializeFloat(context string, input interface{}) (value float64, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.ParseFloat(strValue, 64) + if err != nil { + switch strValue { + case "+Inf": + return math.Inf(1), nil + case "-Inf": + return math.Inf(-1), nil + case "NaN": + return math.NaN(), nil + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/int_convert.go b/ocaml/sdk-gen/go/test_data/int_convert.go new file mode 100644 index 00000000000..0688dffa600 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/int_convert.go @@ -0,0 +1,20 @@ +func serializeInt(context string, value int) (int, error) { + _ = context + return value, nil +} + +func deserializeInt(context string, input interface{}) (value int, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.Atoi(strValue) + if err != nil { + floatValue, err1 := strconv.ParseFloat(strValue, 64) + if err1 == nil { + return int(floatValue), nil + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/interface_convert.go b/ocaml/sdk-gen/go/test_data/interface_convert.go new file mode 100644 index 00000000000..fec2c91d133 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/interface_convert.go @@ -0,0 +1,5 @@ +func deserializeRecordInterface(context string, input interface{}) (inter RecordInterface, err error) { + _ = context + inter = input + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/map_convert.go b/ocaml/sdk-gen/go/test_data/map_convert.go new file mode 100644 index 00000000000..fff680ebc35 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/map_convert.go @@ -0,0 +1,38 @@ +func serializeVIFRefToStringMap(context string, goMap map[VIFRef]string) (xenMap map[string]interface{}, err error) { + xenMap = make(map[string]interface{}) + for goKey, goValue := range goMap { + keyContext := fmt.Sprintf("%s[%s]", context, goKey) + xenKey, err := serializeVIFRef(keyContext, goKey) + if err != nil { + return xenMap, err + } + xenValue, err := serializeString(keyContext, goValue) + if err != nil { + return xenMap, err + } + xenMap[xenKey] = xenValue + } + return +} + +func deserializePBDRefToPBDRecordMap(context string, input interface{}) (goMap map[PBDRef]PBDRecord, err error) { + xenMap, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + goMap = make(map[PBDRef]PBDRecord, len(xenMap)) + for xenKey, xenValue := range xenMap { + keyContext := fmt.Sprintf("%s[%s]", context, xenKey) + goKey, err := deserializePBDRef(keyContext, xenKey) + if err != nil { + return goMap, err + } + goValue, err := deserializePBDRecord(keyContext, xenValue) + if err != nil { + return goMap, err + } + goMap[goKey] = goValue + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/option_convert.go b/ocaml/sdk-gen/go/test_data/option_convert.go new file mode 100644 index 00000000000..4a5ce03a70b --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/option_convert.go @@ -0,0 +1,22 @@ +func serializeOptionSrStatRecord(context string, input OptionSrStatRecord) (option interface{}, err error) { + if input == nil { + return + } + option, err = serializeSrStatRecord(context, *input) + if err != nil { + return + } + return +} + +func deserializeOptionSrStatRecord(context string, input interface{}) (option OptionSrStatRecord, err error) { + if input == nil { + return + } + value, err := deserializeSrStatRecord(context, input) + if err != nil { + return + } + option = &value + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/record_convert.go b/ocaml/sdk-gen/go/test_data/record_convert.go new file mode 100644 index 00000000000..55a8e9c3c90 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/record_convert.go @@ -0,0 +1,35 @@ +func serializeVBDRecord(context string, record VBDRecord) (rpcStruct map[string]interface{}, err error) { + rpcStruct = map[string]interface{}{} + rpcStruct["uuid"], err = serializeString(fmt.Sprintf("%s.%s", context, "uuid"), record.UUID) + if err != nil { + return + } + rpcStruct["allowed_operations"], err = serializeEnumVbdOperationsSet(fmt.Sprintf("%s.%s", context, "allowed_operations"), record.AllowedOperations) + if err != nil { + return + } + return +} + +func deserializeVBDRecord(context string, input interface{}) (record VBDRecord, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + uuidValue, ok := rpcStruct["uuid"] + if ok && uuidValue != nil { + record.UUID, err = deserializeString(fmt.Sprintf("%s.%s", context, "uuid"), uuidValue) + if err != nil { + return + } + } + allowedOperationsValue, ok := rpcStruct["allowed_operations"] + if ok && allowedOperationsValue != nil { + record.AllowedOperations, err = deserializeEnumVbdOperationsSet(fmt.Sprintf("%s.%s", context, "allowed_operations"), allowedOperationsValue) + if err != nil { + return + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/ref_convert.go b/ocaml/sdk-gen/go/test_data/ref_convert.go new file mode 100644 index 00000000000..dc23fc88ffa --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/ref_convert.go @@ -0,0 +1,13 @@ +func serializeVMRef(context string, ref VMRef) (string, error) { + _ = context + return string(ref), nil +} + +func deserializeVMRef(context string, input interface{}) (VMRef, error) { + var ref VMRef + value, ok := input.(string) + if !ok { + return ref, fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return VMRef(value), nil +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/set_convert.go b/ocaml/sdk-gen/go/test_data/set_convert.go new file mode 100644 index 00000000000..1d8adb73764 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/set_convert.go @@ -0,0 +1,30 @@ +func serializeSRRefSet(context string, slice []SRRef) (set []interface{}, err error) { + set = make([]interface{}, len(slice)) + for index, item := range slice { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := serializeSRRef(itemContext, item) + if err != nil { + return set, err + } + set[index] = itemValue + } + return +} + +func deserializeStringSet(context string, input interface{}) (slice []string, err error) { + set, ok := input.([]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "[]interface{}", context, reflect.TypeOf(input), input) + return + } + slice = make([]string, len(set)) + for index, item := range set { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := deserializeString(itemContext, item) + if err != nil { + return slice, err + } + slice[index] = itemValue + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/simple_type_convert.go b/ocaml/sdk-gen/go/test_data/simple_type_convert.go new file mode 100644 index 00000000000..5b482e0a5b7 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/simple_type_convert.go @@ -0,0 +1,31 @@ +func serializeString(context string, value string) (string, error) { + _ = context + return value, nil +} + +func serializeBool(context string, value bool) (bool, error) { + _ = context + return value, nil +} + +func deserializeString(context string, input interface{}) (value string, err error) { + if input == nil { + return + } + value, ok := input.(string) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return +} + +func deserializeBool(context string, input interface{}) (value bool, err error) { + if input == nil { + return + } + value, ok := input.(bool) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "bool", context, reflect.TypeOf(input), input) + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/time_convert.go b/ocaml/sdk-gen/go/test_data/time_convert.go new file mode 100644 index 00000000000..d6da10f4d42 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/time_convert.go @@ -0,0 +1,29 @@ +var timeFormats = []string{time.RFC3339, "20060102T15:04:05Z", "20060102T15:04:05"} + +//nolint:unparam +func serializeTime(context string, value time.Time) (string, error) { + _ = context + return value.Format(time.RFC3339), nil +} + +func deserializeTime(context string, input interface{}) (value time.Time, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + floatValue, err := strconv.ParseFloat(strValue, 64) + if err != nil { + for _, timeFormat := range timeFormats { + value, err = time.Parse(timeFormat, strValue) + if err == nil { + return value, nil + } + } + return + } + unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) + value = time.Unix(unixTimestamp, 0) + + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 37a6e037d63..31964f7751e 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -168,6 +168,132 @@ let verify_msgs_or_errors lst = in List.for_all verify_msg_or_error lst +let verify_simple_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | _ -> + false + +let verify_simple_convert_keys = ["func_name_suffix"; "type"] + +let verify_simple_convert = function + | `O items -> + schema_check verify_simple_convert_keys verify_simple_convert_member items + | _ -> + false + +let verify_option_convert_member = function + | "func_name_suffix", `String _ -> + true + | _ -> + false + +let option_convert_keys = ["func_name_suffix"] + +let verify_option_convert = function + | `O items -> + schema_check option_convert_keys verify_option_convert_member items + | _ -> + false + +let verify_set_convert_member = function + | "func_name_suffix", `String _ + | "type", `String _ + | "item_func_suffix", `String _ -> + true + | _ -> + false + +let convert_set_keys = ["func_name_suffix"; "type"; "item_func_suffix"] + +let verify_set_convert = function + | `O items -> + schema_check convert_set_keys verify_set_convert_member items + | _ -> + false + +let record_field_keys = + ["name"; "name_internal"; "name_exported"; "func_name_suffix"; "type_option"] + +let verify_record_field_member = function + | "name", `String _ + | "name_internal", `String _ + | "func_name_suffix", `String _ + | "type_option", `Bool _ + | "name_exported", `String _ -> + true + | _ -> + false + +let verify_record_field = function + | `O items -> + schema_check record_field_keys verify_record_field_member items + | _ -> + false + +let verify_record_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | "fields", `A fields -> + List.for_all verify_record_field fields + | _ -> + false + +let convert_record_keys = ["func_name_suffix"; "type"; "fields"] + +let verify_record_convert = function + | `O items -> + schema_check convert_record_keys verify_record_convert_member items + | _ -> + false + +let enum_item_keys = ["value"; "name"] + +let verify_enum_item_member = function + | "name", `String _ | "value", `String _ -> + true + | _ -> + false + +let verify_enum_item = function + | `O members -> + schema_check enum_item_keys verify_enum_item_member members + | _ -> + false + +let enum_convert_keys = ["func_name_suffix"; "type"; "items"] + +let verify_enum_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | "items", `A items -> + List.for_all verify_enum_item items + | _ -> + false + +let verify_enum_convert = function + | `O items -> + schema_check enum_convert_keys verify_enum_convert_member items + | _ -> + false + +let map_convert_keys = ["func_name_suffix"; "type"; "key_type"; "value_type"] + +let verify_map_convert_member = function + | "type", `String _ + | "key_type", `String _ + | "func_name_suffix", `String _ + | "value_type", `String _ -> + true + | _ -> + false + +let verify_map_convert = function + | `O items -> + schema_check map_convert_keys verify_map_convert_member items + | _ -> + false + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -303,6 +429,151 @@ let api_messages : Mustache.Json.t = ) ] +let simple_type_convert : Mustache.Json.t = + let array = + [ + `O [("func_name_suffix", `String "String"); ("type", `String "string")] + ; `O [("func_name_suffix", `String "Bool"); ("type", `String "bool")] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let int_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Int"); ("type", `String "int")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let float_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Float"); ("type", `String "float64")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let time_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Time"); ("type", `String "time.Time")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let ref_string_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "VMRef"); ("type", `String "VMRef")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let set_convert : Mustache.Json.t = + let serialize = + [ + `O + [ + ("func_name_suffix", `String "SRRefSet") + ; ("type", `String "SRRef") + ; ("item_func_suffix", `String "SRRef") + ] + ] + in + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "StringSet") + ; ("type", `String "string") + ; ("item_func_suffix", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let record_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "VBDRecord") + ; ("type", `String "VBDRecord") + ; ( "fields" + , `A + [ + `O + [ + ("name", `String "uuid") + ; ("name_internal", `String "uuid") + ; ("name_exported", `String "UUID") + ; ("func_name_suffix", `String "String") + ; ("type_option", `Bool false) + ] + ; `O + [ + ("name", `String "allowed_operations") + ; ("name_internal", `String "allowedOperations") + ; ("name_exported", `String "AllowedOperations") + ; ("func_name_suffix", `String "EnumVbdOperationsSet") + ; ("type_option", `Bool false) + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let map_convert : Mustache.Json.t = + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "PBDRefToPBDRecordMap") + ; ("type", `String "map[PBDRef]PBDRecord") + ; ("key_type", `String "PBDRef") + ; ("value_type", `String "PBDRecord") + ] + ] + in + let serialize = + [ + `O + [ + ("func_name_suffix", `String "VIFRefToStringMap") + ; ("type", `String "map[VIFRef]string") + ; ("key_type", `String "VIFRef") + ; ("value_type", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let enum_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "EnumTaskStatusType") + ; ("type", `String "TaskStatusType") + ; ( "items" + , `A + [ + `O + [ + ("name", `String "TaskStatusTypePending") + ; ("value", `String "pending") + ] + ; `O + [ + ("name", `String "TaskStatusTypeSuccess") + ; ("value", `String "success") + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let option_convert : Mustache.Json.t = + let array = [`O [("func_name_suffix", `String "SrStatRecord")]] in + `O [("serialize", `A array); ("deserialize", `A array)] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -328,6 +599,30 @@ module TemplatesTest = Generic.MakeStateless (struct let api_messages_rendered = string_of_file "api_messages.go" + let simple_type_rendered = string_of_file "simple_type_convert.go" + + let int_convert_rendered = string_of_file "int_convert.go" + + let float_convert_rendered = string_of_file "float_convert.go" + + let time_convert_rendered = string_of_file "time_convert.go" + + let string_ref_rendered = string_of_file "ref_convert.go" + + let set_convert_rendered = string_of_file "set_convert.go" + + let record_convert_rendered = string_of_file "record_convert.go" + + let interface_convert_rendered = string_of_file "interface_convert.go" + + let map_convert_rendered = string_of_file "map_convert.go" + + let enum_convert_rendered = string_of_file "enum_convert.go" + + let batch_convert_rendered = string_of_file "batch_convert.go" + + let option_convert_rendered = string_of_file "option_convert.go" + let tests = `QuickAndAutoDocumented [ @@ -336,6 +631,22 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("Enum.mustache", enums), enums_rendered) ; (("APIErrors.mustache", api_errors), api_errors_rendered) ; (("APIMessages.mustache", api_messages), api_messages_rendered) + ; ( ("ConvertSimpleType.mustache", simple_type_convert) + , simple_type_rendered + ) + ; (("ConvertInt.mustache", int_convert), int_convert_rendered) + ; (("ConvertFloat.mustache", float_convert), float_convert_rendered) + ; (("ConvertTime.mustache", time_convert), time_convert_rendered) + ; (("ConvertRef.mustache", ref_string_convert), string_ref_rendered) + ; (("ConvertSet.mustache", set_convert), set_convert_rendered) + ; (("ConvertRecord.mustache", record_convert), record_convert_rendered) + ; ( ("ConvertInterface.mustache", Convert.interface) + , interface_convert_rendered + ) + ; (("ConvertMap.mustache", map_convert), map_convert_rendered) + ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) + ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) + ; (("ConvertOption.mustache", option_convert), option_convert_rendered) ] end) @@ -363,12 +674,76 @@ module TestGeneratedJson = struct ] end +module TestConvertGeneratedJson = struct + open Convert + + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) + + let param_types = TypesOfMessages.of_params objects + + let result_types = TypesOfMessages.of_results objects + + let verify_func = function + | Simple _ | Int _ | Float _ | Time _ | Ref _ -> + verify_simple_convert + | Option _ -> + verify_option_convert + | Set _ -> + verify_set_convert + | Record _ -> + verify_record_convert + | Enum _ -> + verify_enum_convert + | Map _ -> + verify_map_convert + + let convert_param_name = function + | Simple _ -> + "simple" + | Int _ -> + "int" + | Float _ -> + "float" + | Time _ -> + "time" + | Ref _ -> + "ref" + | Option _ -> + "option" + | Set _ -> + "set" + | Record _ -> + "record" + | Enum _ -> + "enum" + | Map _ -> + "map" + + let test types () = + List.iter + (fun ty -> + let param = Convert.of_ty ty in + let obj = Convert.to_json param in + let verify_func = verify_func param in + verify (convert_param_name param) verify_func obj + ) + types + + let tests = + [ + ("serialize", `Quick, test param_types) + ; ("deserialize", `Quick, test result_types) + ] +end + let tests = make_suite "gen_go_binding_" [ ("snake_to_camel", SnakeToCamelTest.tests) ; ("templates", TemplatesTest.tests) ; ("generated_mustache_jsons", TestGeneratedJson.tests) + ; ("generated_convert_jsons", TestConvertGeneratedJson.tests) ] let () = Alcotest.run "Gen go binding" tests From 6370b36dae572e471bb108b9dce69be83a790639 Mon Sep 17 00:00:00 2001 From: xueqingz Date: Mon, 22 Apr 2024 08:30:57 +0000 Subject: [PATCH 30/99] CP-47355, CP-47360: generate mustache template for xapi data module class messages Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/autogen/src/go.mod | 2 +- ocaml/sdk-gen/go/templates/Methods.mustache | 54 ++++++++++++++++ .../go/templates/SessionMethod.mustache | 64 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 ocaml/sdk-gen/go/templates/Methods.mustache create mode 100644 ocaml/sdk-gen/go/templates/SessionMethod.mustache diff --git a/ocaml/sdk-gen/go/autogen/src/go.mod b/ocaml/sdk-gen/go/autogen/src/go.mod index 0d33115cf62..9a8b7f9c133 100644 --- a/ocaml/sdk-gen/go/autogen/src/go.mod +++ b/ocaml/sdk-gen/go/autogen/src/go.mod @@ -1,3 +1,3 @@ module go/xenapi -go 1.22.0 +go 1.22.2 diff --git a/ocaml/sdk-gen/go/templates/Methods.mustache b/ocaml/sdk-gen/go/templates/Methods.mustache new file mode 100644 index 00000000000..32a0a5c8982 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Methods.mustache @@ -0,0 +1,54 @@ +{{#messages}} +// {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func ({{name_internal}}) {{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) ({{#result}}retval {{type}}, {{/result}}err error) { + method := "{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + if err != nil { + return + } +{{/params}} + {{#result}}result, err := {{/result}}{{^result}}_, err = {{/result}}session.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) +{{#result}} + if err != nil { + return + } + retval, err = deserialize{{func_name_suffix}}(method+" -> ", result) +{{/result}} + return +} + +{{#async}} +// Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func ({{name_internal}}) Async{{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) (retval TaskRef, err error) { + method := "Async.{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + if err != nil { + return + } +{{/params}} + result, err := session.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} + +{{/async}} +{{/messages}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/SessionMethod.mustache b/ocaml/sdk-gen/go/templates/SessionMethod.mustache new file mode 100644 index 00000000000..b73fb057379 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/SessionMethod.mustache @@ -0,0 +1,64 @@ +{{#messages}} +// {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func (class *Session) {{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) ({{#result}}retval {{type}}, {{/result}}err error) { + method := "{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + if err != nil { + return + } +{{/params}} + {{#result}}result, err := {{/result}}{{^result}}_, err = {{/result}}class.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) +{{#result}} + if err != nil { + return + } + retval, err = deserialize{{func_name_suffix}}(method+" -> ", result) +{{/result}} +{{#session_login}} + if err != nil { + return + } + class.ref = retval + err = setSessionDetails(class) +{{/session_login}} +{{#session_logout}} + class.ref = "" +{{/session_logout}} + return +} + +{{#async}} +// Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#has_error}} +// +// Errors: +{{/has_error}} +{{#errors}} +// {{name}} - {{doc}} +{{/errors}} +func (class *Session) Async{{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) (retval TaskRef, err error) { + method := "Async.{{class_name}}.{{method_name}}" +{{#params}} + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + if err != nil { + return + } +{{/params}} + result, err := class.client.sendCall(method{{#params}}, {{name_internal}}Arg{{/params}}) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} + +{{/async}} +{{/messages}} \ No newline at end of file From f2b241ebbb48dc3d820635dcb98c7de5b996ee1b Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 24 Apr 2024 09:56:36 +0800 Subject: [PATCH 31/99] CP-47354: Generate messages functions Golang code for all classes Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 15 +- ocaml/sdk-gen/go/gen_go_helper.ml | 174 +++++++++- ocaml/sdk-gen/go/gen_go_helper.mli | 10 + ocaml/sdk-gen/go/test_data/methods.go | 37 +++ ocaml/sdk-gen/go/test_data/session_method.go | 38 +++ ocaml/sdk-gen/go/test_gen_go.ml | 321 ++++++++++++++++++- 6 files changed, 572 insertions(+), 23 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/methods.go create mode 100644 ocaml/sdk-gen/go/test_data/session_method.go diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 7e7a6ad726f..83f181328b5 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -104,7 +104,20 @@ let main destdir = render_template "FileHeader.mustache" obj ~newline:true () in let record_rendered = render_template "Record.mustache" obj () in - let rendered = header_rendered ^ record_rendered in + let methods_rendered = + if name = "session" then + render_template "SessionMethod.mustache" obj () + else + render_template "Methods.mustache" obj () + in + let rendered = + let first_half = header_rendered ^ record_rendered in + match methods_rendered with + | "" -> + first_half + | _ -> + first_half ^ "\n" ^ methods_rendered + in let output_file = name ^ ".go" in generate_file ~rendered ~destdir ~output_file ) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 92f2bf97dd9..731feef8586 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -39,7 +39,7 @@ let render_template template_file json ?(newline = false) () = let templ = string_of_file (templates_dir // template_file) |> Mustache.of_string in - let renndered = Mustache.render templ json in + let renndered = Mustache.render ~strict:true templ json in if newline then renndered ^ "\n" else renndered let generate_file ~rendered ~destdir ~output_file = @@ -60,7 +60,7 @@ module Json = struct let merge_maps m maps = List.fold_left (fun acc map -> StringMap.union choose_enum acc map) m maps - let rec func_name_suffix ty = + let rec suffix_of_type ty = match ty with | SecretString | String -> "String" @@ -75,17 +75,17 @@ module Json = struct | Enum (name, _) -> "Enum" ^ snake_to_camel name | Set ty -> - func_name_suffix ty ^ "Set" + suffix_of_type ty ^ "Set" | Map (ty1, ty2) -> - let k_suffix = func_name_suffix ty1 in - let v_suffix = func_name_suffix ty2 in + let k_suffix = suffix_of_type ty1 in + let v_suffix = suffix_of_type ty2 in k_suffix ^ "To" ^ v_suffix ^ "Map" | Ref r -> snake_to_camel r ^ "Ref" | Record r -> snake_to_camel r ^ "Record" | Option ty -> - func_name_suffix ty + suffix_of_type ty let rec string_of_ty_with_enums ty : string * enums = match ty with @@ -203,9 +203,149 @@ module Json = struct | _ -> [("event", `Null); ("session", `Null)] + let of_result obj msg = + match msg.msg_result with + | None -> + `Null + | Some (t, _d) -> + if obj.name = "event" && String.lowercase_ascii msg.msg_name = "from" + then + `O + [ + ("type", `String "EventBatch") + ; ("func_name_suffix", `String "EventBatch") + ] + else + let t', _ = string_of_ty_with_enums t in + `O + [ + ("type", `String t') + ; ("func_name_suffix", `String (suffix_of_type t)) + ] + + let of_params params = + let name_internal name = + let name = name |> snake_to_camel |> String.uncapitalize_ascii in + match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name + in + let of_param param = + let suffix_of_type = suffix_of_type param.param_type in + let t, _e = string_of_ty_with_enums param.param_type in + let name = param.param_name in + [ + ("is_session_id", `Bool (name = "session_id")) + ; ("type", `String t) + ; ("name", `String name) + ; ("name_internal", `String (name_internal name)) + ; ("doc", `String param.param_doc) + ; ("func_name_suffix", `String suffix_of_type) + ] + in + (* We use ',' to seprate params in Go function, we should ignore ',' before first param, + for example `func(a type1, b type2)` is wanted rather than `func(, a type1, b type2)`. + *) + let add_first = function + | head :: rest -> + let head = `O (("first", `Bool true) :: of_param head) in + let rest = + List.map + (fun item -> `O (("first", `Bool false) :: of_param item)) + rest + in + head :: rest + | [] -> + [] + in + `A (add_first params) + + let of_error e = `O [("name", `String e.err_name); ("doc", `String e.err_doc)] + + let of_errors = function + | [] -> + `Null + | errors -> + `A (List.map of_error errors) + + let add_session_info class_name method_name = + match (class_name, method_name) with + | "session", "login_with_password" + | "session", "slave_local_login_with_password" -> + [("session_login", `Bool true); ("session_logout", `Bool false)] + | "session", "logout" | "session", "local_logout" -> + [("session_login", `Bool false); ("session_logout", `Bool true)] + | _ -> + [("session_login", `Bool false); ("session_logout", `Bool false)] + + let desc_of_msg msg ctor_fields = + let ctor = + if msg.msg_tag = FromObject Make then + Printf.sprintf " The constructor args are: %s (* = non-optional)." + ctor_fields + else + "" + in + match msg.msg_doc ^ ctor with + | "" -> + `Null + | desc -> + `String (String.trim desc) + + let ctor_fields_of_obj obj = + Datamodel_utils.fields_of_obj obj + |> List.filter (function + | {qualifier= StaticRO | RW; _} -> + true + | _ -> + false + ) + |> List.map (fun f -> + String.concat "_" f.full_name + ^ if f.default_value = None then "*" else "" + ) + |> String.concat ", " + + let messages_of_obj obj = + let ctor_fields = ctor_fields_of_obj obj in + let params_in_msg msg = + if msg.msg_session then + session_id :: msg.msg_params + else + msg.msg_params + in + List.map + (fun msg -> + let params = params_in_msg msg |> of_params in + let base_assoc_list = + [ + ("method_name", `String msg.msg_name) + ; ("class_name", `String obj.name) + ; ("class_name_exported", `String (snake_to_camel obj.name)) + ; ("method_name_exported", `String (snake_to_camel msg.msg_name)) + ; ("description", desc_of_msg msg ctor_fields) + ; ("result", of_result obj msg) + ; ("params", params) + ; ("errors", of_errors msg.msg_errors) + ; ("has_error", `Bool (msg.msg_errors <> [])) + ; ("async", `Bool msg.msg_async) + ] + in + (* Since the param of `session *Session` isn't needed in functions of session object, + we add a special "func_params" field for session object to ignore `session *Session`.*) + if obj.name = "session" then + `O + (("func_params", msg.msg_params |> of_params) + :: (add_session_info obj.name msg.msg_name @ base_assoc_list) + ) + else + `O base_assoc_list + ) + obj.messages + let xenapi objs = List.map (fun obj -> + let obj_name = snake_to_camel obj.name in + let name_internal = String.uncapitalize_ascii obj_name in let fields = Datamodel_utils.fields_of_obj obj in let types = List.map (fun field -> field.ty) fields in let modules = @@ -213,12 +353,14 @@ module Json = struct in let base_assoc_list = [ - ("name", `String (snake_to_camel obj.name)) + ("name", `String obj_name) + ; ("name_internal", `String name_internal) ; ("description", `String (String.trim obj.description)) ; ( "fields" , `A (get_event_snapshot obj.name @ List.map of_field fields) ) ; ("modules", modules) + ; ("messages", `A (messages_of_obj obj)) ] in let assoc_list = base_assoc_list @ get_event_session_value obj.name in @@ -369,7 +511,7 @@ module Convert = struct |> Option.value ~default:[] |> List.rev_map (fun field -> ( String.concat "_" field.full_name - , Json.func_name_suffix field.ty + , Json.suffix_of_type field.ty , match field.ty with Option _ -> true | _ -> false ) ) @@ -395,13 +537,13 @@ module Convert = struct let items = List.map (fun (k, _) -> {value= k; name= name ^ snake_to_camel k}) kv in - Enum {func_suffix= Json.func_name_suffix ty; value_ty= name; items} + Enum {func_suffix= Json.suffix_of_type ty; value_ty= name; items} | Set ty as set -> - let fp_ty = Json.func_name_suffix ty in + let fp_ty = Json.suffix_of_type ty in let ty, _ = Json.string_of_ty_with_enums ty in Set { - func_suffix= Json.func_name_suffix set + func_suffix= Json.suffix_of_type set ; value_ty= ty ; item_fp_type= fp_ty } @@ -409,13 +551,13 @@ module Convert = struct let name, _ = Json.string_of_ty_with_enums ty in Map { - func_suffix= Json.func_name_suffix ty + func_suffix= Json.suffix_of_type ty ; value_ty= name - ; key_ty= Json.func_name_suffix ty1 - ; val_ty= Json.func_name_suffix ty2 + ; key_ty= Json.suffix_of_type ty1 + ; val_ty= Json.suffix_of_type ty2 } | Ref _ as ty -> - let name = Json.func_name_suffix ty in + let name = Json.suffix_of_type ty in Ref {func_suffix= name; value_ty= name} | Record r -> let name = snake_to_camel r ^ "Record" in @@ -435,7 +577,7 @@ module Convert = struct in Record {func_suffix= name; value_ty= name; fields} | Option ty -> - Option {func_suffix= Json.func_name_suffix ty} + Option {func_suffix= Json.suffix_of_type ty} let of_serialize params = `O [("serialize", `A [to_json params]); ("deserialize", `Null)] diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 29a301b965a..ffa5ac673c9 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -22,6 +22,16 @@ val generate_file : rendered:string -> destdir:string -> output_file:string -> unit module Json : sig + type enum = (string * string) list + + module StringMap : Map.S with type key = string + + type enums = enum StringMap.t + + val suffix_of_type : Datamodel_types.ty -> string + + val string_of_ty_with_enums : Datamodel_types.ty -> string * enums + val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list val all_enums : Datamodel_types.obj list -> Mustache.Json.t diff --git a/ocaml/sdk-gen/go/test_data/methods.go b/ocaml/sdk-gen/go/test_data/methods.go new file mode 100644 index 00000000000..3e80d71b0f7 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/methods.go @@ -0,0 +1,37 @@ +// GetLog: GetLog Get the host log file +func (host) GetLog(session *Session, host HostRef) (retval string, err error) { + method := "host.get_log" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) + if err != nil { + return + } + hostArg, err := serializeHostRef(fmt.Sprintf("%s(%s)", method, "host"), host) + if err != nil { + return + } + result, err := session.client.sendCall(method, sessionIDArg, hostArg) + if err != nil { + return + } + retval, err = deserializeString(method+" -> ", result) + return +} + +// AsyncGetLog: GetLog Get the host log file +func (host) AsyncGetLog(session *Session, host HostRef) (retval TaskRef, err error) { + method := "Async.host.get_log" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) + if err != nil { + return + } + hostArg, err := serializeHostRef(fmt.Sprintf("%s(%s)", method, "host"), host) + if err != nil { + return + } + result, err := session.client.sendCall(method, sessionIDArg, hostArg) + if err != nil { + return + } + retval, err = deserializeTaskRef(method+" -> ", result) + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/session_method.go b/ocaml/sdk-gen/go/test_data/session_method.go new file mode 100644 index 00000000000..b476f4606a8 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/session_method.go @@ -0,0 +1,38 @@ +// LoginWithPassword: Attempt to authenticate the user); returning a session reference if successful +// +// Errors: +// SESSION_AUTHENTICATION_FAILED - The credentials given by the user are incorrect +func (class *Session) LoginWithPassword(uname string, pwd string) (retval SessionRef, err error) { + method := "session.login_with_password" + unameArg, err := serializeString(fmt.Sprintf("%s(%s)", method, "uname"), uname) + if err != nil { + return + } + pwdArg, err := serializeString(fmt.Sprintf("%s(%s)", method, "pwd"), pwd) + if err != nil { + return + } + result, err := class.client.sendCall(method, unameArg, pwdArg) + if err != nil { + return + } + retval, err = deserializeSessionRef(method+" -> ", result) + if err != nil { + return + } + class.ref = retval + err = setSessionDetails(class) + return +} + +// Logout: Logout Log out of a session +func (class *Session) Logout() (err error) { + method := "session.logout" + sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), class.ref) + if err != nil { + return + } + _, err = class.client.sendCall(method, sessionIDArg) + class.ref = "" + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 31964f7751e..590b4e3b77e 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -51,7 +51,6 @@ let schema_check keys checker members = let keys' = List.map (fun (k, _) -> k) members in compare_keys keys keys' && List.for_all checker members -(* field *) let verify_field_member = function | "name", `String _ | "description", `String _ | "type", `String _ -> true @@ -66,7 +65,134 @@ let verify_field = function | _ -> false -(* module *) +let result_keys = ["type"; "func_partial_type"] + +let verify_result_member = function + | "type", `String _ | "func_partial_type", `String _ -> + true + | _ -> + false + +let error_keys = ["name"; "doc"] + +let verify_error_member = function + | "name", `String _ | "doc", `String _ -> + true + | _ -> + false + +let verify_error = function + | `O error -> + schema_check error_keys verify_error_member error + | _ -> + false + +let param_keys = + [ + "is_session_id" + ; "type" + ; "name" + ; "name_internal" + ; "doc" + ; "func_partial_type" + ; "first" + ] + +let verify_param_member = function + | "is_session_id", `Bool _ + | "first", `Bool _ + | "type", `String _ + | "name", `String _ + | "name_internal", `String _ + | "doc", `String _ + | "func_partial_type", `String _ -> + true + | _ -> + false + +let verify_param = function + | `O param -> + schema_check param_keys verify_param_member param + | _ -> + false + +let verify_message_member = function + | "method_name", `String _ + | "class_name", `String _ + | "class_name_exported", `String _ + | "method_name_exported", `String _ -> + true + | "description", `String _ | "description", `Null -> + true + | "result", `Null -> + true + | "result", `O result -> + schema_check result_keys verify_result_member result + | "params", `A params -> + List.for_all verify_param params + | "errors", `A errors -> + List.for_all verify_error errors + | "async", `Bool _ | "has_error", `Bool _ | "errors", `Null -> + true + | _ -> + false + +let verify_sesseion_message_member = function + | "method_name", `String _ + | "class_name", `String _ + | "class_name_exported", `String _ + | "method_name_exported", `String _ -> + true + | "description", `String _ | "description", `Null -> + true + | "result", `Null -> + true + | "result", `O result -> + schema_check result_keys verify_result_member result + | "params", `A params -> + List.for_all verify_param params + | "header_params", `A params -> + List.for_all verify_param params + | "errors", `A errors -> + List.for_all verify_error errors + | "async", `Bool _ + | "has_error", `Bool _ + | "session_login", `Bool _ + | "session_logout", `Bool _ + | "errors", `Null -> + true + | _ -> + false + +let message_keys = + [ + "method_name" + ; "class_name" + ; "class_name_exported" + ; "method_name_exported" + ; "description" + ; "result" + ; "params" + ; "errors" + ; "has_error" + ; "async" + ] + +let session_message_keys = + ["session_login"; "session_logout"; "header_params"] @ message_keys + +let verify_message = function + | `O members -> + let class_name = + List.assoc_opt "class_name" members |> Option.value ~default:`Null + in + if class_name <> `String "session" then + schema_check message_keys verify_message_member members + else + schema_check session_message_keys verify_sesseion_message_member members + | _ -> + false + let verify_module_member = function | "name", `String _ -> true @@ -83,7 +209,6 @@ let verify_modules_item = function | _ -> false -(* modules *) let modules_keys = ["import"; "items"] let verify_modules_member = function @@ -96,7 +221,6 @@ let verify_modules_member = function let enum_values_keys = ["value"; "doc"; "name"; "type"] -(* enums *) let verify_enum_values_member = function | "value", `String _ | "doc", `String _ @@ -136,7 +260,7 @@ let verify_enums : Mustache.Json.t -> bool = function (* obj *) let verify_obj_member = function - | "name", `String _ | "description", `String _ -> + | "name", `String _ | "description", `String _ | "name_internal", `String _ -> true | "event", `Bool _ | "event", `Null -> true @@ -144,6 +268,8 @@ let verify_obj_member = function true | "fields", `A fields -> List.for_all verify_field fields + | "messages", `A messages -> + List.for_all verify_message messages | "modules", `Null -> true | "modules", `O members -> @@ -151,7 +277,17 @@ let verify_obj_member = function | _ -> false -let obj_keys = ["name"; "description"; "fields"; "modules"; "event"; "session"] +let obj_keys = + [ + "name" + ; "description" + ; "name_internal" + ; "fields" + ; "messages" + ; "modules" + ; "event" + ; "session" + ] let verify_obj = function | `O members -> @@ -574,6 +710,173 @@ let option_convert : Mustache.Json.t = let array = [`O [("func_name_suffix", `String "SrStatRecord")]] in `O [("serialize", `A array); ("deserialize", `A array)] +let session_messages : Mustache.Json.t = + `O + [ + ( "messages" + , `A + [ + `O + [ + ("session_login", `Bool true) + ; ("session_logout", `Bool false) + ; ("class_name", `String "session") + ; ("name_internal", `String "") + ; ("method_name", `String "login_with_password") + ; ("method_name_exported", `String "LoginWithPassword") + ; ( "description" + , `String + "Attempt to authenticate the user); returning a session \ + reference if successful" + ) + ; ("async", `Bool false) + ; ( "header_params" + , `A + [ + `O + [ + ("type", `String "string") + ; ("name", `String "uname") + ; ("name_internal", `String "uname") + ; ("func_partial_type", `String "String") + ; ("first", `Bool true) + ; ("is_session_id", `Bool false) + ] + ; `O + [ + ("type", `String "string") + ; ("name", `String "pwd") + ; ("name_internal", `String "pwd") + ; ("func_name_suffix", `String "String") + ; ("is_session_id", `Bool false) + ] + ] + ) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "string") + ; ("name", `String "uname") + ; ("name_internal", `String "uname") + ; ("func_partial_type", `String "String") + ; ("first", `Bool true) + ; ("is_session_id", `Bool false) + ] + ; `O + [ + ("type", `String "string") + ; ("name", `String "pwd") + ; ("name_internal", `String "pwd") + ; ("func_name_suffix", `String "String") + ; ("is_session_id", `Bool false) + ] + ] + ) + ; ( "result" + , `O + [ + ("type", `String "SessionRef") + ; ("func_partial_type", `String "SessionRef") + ] + ) + ; ("has_error", `Bool true) + ; ( "errors" + , `A + [ + `O + [ + ("name", `String "SESSION_AUTHENTICATION_FAILED") + ; ( "doc" + , `String + "The credentials given by the user are incorrect" + ) + ] + ] + ) + ] + ; `O + [ + ("session_logout", `Bool true) + ; ("session_login", `Bool false) + ; ("class_name", `String "session") + ; ("class_name_exported", `String "Session") + ; ("method_name", `String "logout") + ; ("method_name_exported", `String "Logout") + ; ("description", `String "Logout Log out of a session") + ; ("async", `Bool false) + ; ("func_params", `A []) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "SessionRef") + ; ("name", `String "session_id") + ; ("name_internal", `String "sessionID") + ; ("func_name_suffix", `String "SessionRef") + ; ("is_session_id", `Bool true) + ] + ] + ) + ; ("result", `Null) + ; ("has_error", `Bool false) + ; ("errors", `A []) + ] + ] + ) + ] + +let messages : Mustache.Json.t = + `O + [ + ( "messages" + , `A + [ + `O + [ + ("class_name", `String "host") + ; ("name_internal", `String "host") + ; ("method_name", `String "get_log") + ; ("method_name_exported", `String "GetLog") + ; ("description", `String "GetLog Get the host log file") + ; ("async", `Bool true) + ; ( "params" + , `A + [ + `O + [ + ("type", `String "SessionRef") + ; ("name", `String "session_id") + ; ("name_internal", `String "sessionID") + ; ("func_name_suffix", `String "SessionRef") + ; ("first", `Bool true) + ] + ; `O + [ + ("type", `String "HostRef") + ; ("name", `String "host") + ; ("name_internal", `String "host") + ; ("func_name_suffix", `String "HostRef") + ; ("first", `Bool false) + ] + ] + ) + ; ( "result" + , `O + [ + ("type", `String "string") + ; ("func_partial_type", `String "String") + ] + ) + ; ("has_error", `Bool false) + ; ("errors", `A []) + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -595,6 +898,10 @@ module TemplatesTest = Generic.MakeStateless (struct let enums_rendered = string_of_file "enum.go" + let methods_rendered = string_of_file "methods.go" + + let session_method_rendered = string_of_file "session_method.go" + let api_errors_rendered = string_of_file "api_errors.go" let api_messages_rendered = string_of_file "api_messages.go" @@ -629,6 +936,8 @@ module TemplatesTest = Generic.MakeStateless (struct (("FileHeader.mustache", header), file_header_rendered) ; (("Record.mustache", record), record_rendered) ; (("Enum.mustache", enums), enums_rendered) + ; (("Methods.mustache", messages), methods_rendered) + ; (("SessionMethod.mustache", session_messages), session_method_rendered) ; (("APIErrors.mustache", api_errors), api_errors_rendered) ; (("APIMessages.mustache", api_messages), api_messages_rendered) ; ( ("ConvertSimpleType.mustache", simple_type_convert) From 51f2e94831f61110635be7d70c55c1379a747b49 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 6 May 2024 20:15:36 +0800 Subject: [PATCH 32/99] CP-47354: add unit tests for `func_name_suffix` and `string_of_ty_with_enums` Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 145 +++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 11 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 590b4e3b77e..7609452ecfa 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -65,10 +65,10 @@ let verify_field = function | _ -> false -let result_keys = ["type"; "func_partial_type"] +let result_keys = ["type"; "func_name_suffix"] let verify_result_member = function - | "type", `String _ | "func_partial_type", `String _ -> + | "type", `String _ | "func_name_suffix", `String _ -> true | _ -> false @@ -94,7 +94,7 @@ let param_keys = ; "name" ; "name_internal" ; "doc" - ; "func_partial_type" + ; "func_name_suffix" ; "first" ] @@ -105,7 +105,7 @@ let verify_param_member = function | "name", `String _ | "name_internal", `String _ | "doc", `String _ - | "func_partial_type", `String _ -> + | "func_name_suffix", `String _ -> true | _ -> false @@ -151,7 +151,7 @@ let verify_sesseion_message_member = function schema_check result_keys verify_result_member result | "params", `A params -> List.for_all verify_param params - | "header_params", `A params -> + | "func_params", `A params -> List.for_all verify_param params | "errors", `A errors -> List.for_all verify_error errors @@ -179,7 +179,7 @@ let message_keys = ] let session_message_keys = - ["session_login"; "session_logout"; "header_params"] @ message_keys + ["session_login"; "session_logout"; "func_params"] @ message_keys let verify_message = function | `O members -> @@ -730,7 +730,7 @@ let session_messages : Mustache.Json.t = reference if successful" ) ; ("async", `Bool false) - ; ( "header_params" + ; ( "func_params" , `A [ `O @@ -738,7 +738,7 @@ let session_messages : Mustache.Json.t = ("type", `String "string") ; ("name", `String "uname") ; ("name_internal", `String "uname") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ; ("first", `Bool true) ; ("is_session_id", `Bool false) ] @@ -760,7 +760,7 @@ let session_messages : Mustache.Json.t = ("type", `String "string") ; ("name", `String "uname") ; ("name_internal", `String "uname") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ; ("first", `Bool true) ; ("is_session_id", `Bool false) ] @@ -778,7 +778,7 @@ let session_messages : Mustache.Json.t = , `O [ ("type", `String "SessionRef") - ; ("func_partial_type", `String "SessionRef") + ; ("func_name_suffix", `String "SessionRef") ] ) ; ("has_error", `Bool true) @@ -851,6 +851,8 @@ let messages : Mustache.Json.t = ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") ; ("func_name_suffix", `String "SessionRef") + ; ("session", `Bool true) + ; ("session_class", `Bool false) ; ("first", `Bool true) ] ; `O @@ -867,7 +869,7 @@ let messages : Mustache.Json.t = , `O [ ("type", `String "string") - ; ("func_partial_type", `String "String") + ; ("func_name_suffix", `String "String") ] ) ; ("has_error", `Bool false) @@ -983,6 +985,38 @@ module TestGeneratedJson = struct ] end +module SuffixOfTypeTest = Generic.MakeStateless (struct + open Datamodel_types + + module Io = struct + type input_t = ty + + type output_t = string + + let string_of_input_t = Json.suffix_of_type + + let string_of_output_t = Test_printers.string + end + + let transform = Json.suffix_of_type + + let tests = + `QuickAndAutoDocumented + [ + (SecretString, "String") + ; (String, "String") + ; (Int, "Int") + ; (Float, "Float") + ; (Bool, "Bool") + ; (Enum ("update_sync", [("a", "b"); ("c", "d")]), "EnumUpdateSync") + ; (Set String, "StringSet") + ; (Map (Int, String), "IntToStringMap") + ; (Ref "pool", "PoolRef") + ; (Record "pool", "PoolRecord") + ; (Option String, "String") + ] +end) + module TestConvertGeneratedJson = struct open Convert @@ -1046,10 +1080,99 @@ module TestConvertGeneratedJson = struct ] end +module StringOfTyWithEnumsTest = struct + open Datamodel_types + module StringMap = Json.StringMap + + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) + + let verify_string (ty, enums) = ty = "string" && enums = StringMap.empty + + let test_string () = + let ty, enums = Json.string_of_ty_with_enums String in + verify "String" verify_string (ty, enums) + + let test_secret_string () = + let ty, enums = Json.string_of_ty_with_enums SecretString in + verify "SecretString" verify_string (ty, enums) + + let verify_float (ty, enums) = ty = "float64" && enums = StringMap.empty + + let test_float () = + let ty, enums = Json.string_of_ty_with_enums Float in + verify "Float" verify_float (ty, enums) + + let verify_bool (ty, enums) = ty = "bool" && enums = StringMap.empty + + let test_bool () = + let ty, enums = Json.string_of_ty_with_enums Bool in + verify "bool" verify_bool (ty, enums) + + let verify_datetime (ty, enums) = ty = "time.Time" && enums = StringMap.empty + + let test_datetime () = + let ty, enums = Json.string_of_ty_with_enums DateTime in + verify "datetime" verify_datetime (ty, enums) + + let enum_lst = [("a", "b"); ("c", "d")] + + let verify_enum (ty, enums) = + ty = "UpdateSync" && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_enum () = + let ty, enums = + Json.string_of_ty_with_enums (Enum ("update_sync", enum_lst)) + in + verify "enum" verify_enum (ty, enums) + + let verify_ref (ty, enums) = ty = "PoolRef" && enums = StringMap.empty + + let test_ref () = + let ty, enums = Json.string_of_ty_with_enums (Ref "pool") in + verify "ref" verify_ref (ty, enums) + + let verify_record (ty, enums) = ty = "PoolRecord" && enums = StringMap.empty + + let test_record () = + let ty, enums = Json.string_of_ty_with_enums (Record "pool") in + verify "datetime" verify_record (ty, enums) + + let test_option () = + let ty, enums = Json.string_of_ty_with_enums (Option String) in + verify "datetime" verify_string (ty, enums) + + let verify_map (ty, enums) = + ty = "map[int]UpdateSync" + && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_map () = + let ty, enums = + Json.string_of_ty_with_enums (Map (Int, Enum ("update_sync", enum_lst))) + in + verify "map" verify_map (ty, enums) + + let tests = + [ + ("String", `Quick, test_string) + ; ("SecretString", `Quick, test_secret_string) + ; ("Float", `Quick, test_float) + ; ("Bool", `Quick, test_bool) + ; ("DateTime", `Quick, test_datetime) + ; ("Enum", `Quick, test_enum) + ; ("Ref", `Quick, test_ref) + ; ("Record", `Quick, test_record) + ; ("Option", `Quick, test_option) + ; ("Map", `Quick, test_map) + ] +end + let tests = make_suite "gen_go_binding_" [ ("snake_to_camel", SnakeToCamelTest.tests) + ; ("suffix_of_type", SuffixOfTypeTest.tests) + ; ("string_of_ty_with_enums", StringOfTyWithEnumsTest.tests) ; ("templates", TemplatesTest.tests) ; ("generated_mustache_jsons", TestGeneratedJson.tests) ; ("generated_convert_jsons", TestConvertGeneratedJson.tests) From 0676c3c89c793391a40ee032eaebf620f2a71b6b Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:35:12 +0800 Subject: [PATCH 33/99] CP-48855: update templates (APIErrors, APIMessages, Record) Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/templates/APIErrors.mustache | 3 ++- .../sdk-gen/go/templates/APIMessages.mustache | 2 +- ocaml/sdk-gen/go/templates/Record.mustache | 18 ++++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ocaml/sdk-gen/go/templates/APIErrors.mustache b/ocaml/sdk-gen/go/templates/APIErrors.mustache index 8128b5ef185..9bce9a085dc 100644 --- a/ocaml/sdk-gen/go/templates/APIErrors.mustache +++ b/ocaml/sdk-gen/go/templates/APIErrors.mustache @@ -1,6 +1,7 @@ +//nolint:gosec const ( {{#api_errors}} // - ERR_{{name}} = "{{name}}" + Error{{name}} = "{{value}}" {{/api_errors}} ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/APIMessages.mustache b/ocaml/sdk-gen/go/templates/APIMessages.mustache index 172e4e416c4..3bac101ff6f 100644 --- a/ocaml/sdk-gen/go/templates/APIMessages.mustache +++ b/ocaml/sdk-gen/go/templates/APIMessages.mustache @@ -1,6 +1,6 @@ const ( {{#api_messages}} // - MSG_{{name}} = "{{name}}" + Message{{name}} = "{{value}}" {{/api_messages}} ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/Record.mustache b/ocaml/sdk-gen/go/templates/Record.mustache index e4d874ba978..b30e234e1cb 100644 --- a/ocaml/sdk-gen/go/templates/Record.mustache +++ b/ocaml/sdk-gen/go/templates/Record.mustache @@ -21,21 +21,23 @@ type EventBatch struct { // {{.}} {{/description}} {{#session}} -type {{name}}Class struct { - client *rpcClient - ref SessionRef +type {{name}} struct { + APIVersion APIVersion + client *rpcClient + ref SessionRef + XAPIVersion string } -func NewSession(opts *ClientOpts) *SessionClass { - client := NewJsonRPCClient(opts) - var session SessionClass +func NewSession(opts *ClientOpts) *Session { + client := newJSONRPCClient(opts) + var session Session session.client = client return &session } {{/session}} {{^session}} -type {{name}}Class struct{} +type {{name_internal}} struct{} -var {{name}} *{{name}}Class +var {{name}} {{name_internal}} {{/session}} \ No newline at end of file From 327260c4d3466586bbd1bb24ec628cc2d1a9afc1 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:44:57 +0800 Subject: [PATCH 34/99] CP-48855: adjust generated json for templates changed changed templates: APIVersions.mustache,APIErrors.mustache, Record.mustache Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 29 +++++++++++++++-- ocaml/sdk-gen/go/test_data/api_errors.go | 7 +++-- ocaml/sdk-gen/go/test_data/api_messages.go | 4 +-- ocaml/sdk-gen/go/test_data/record.go | 14 +++++---- ocaml/sdk-gen/go/test_gen_go.ml | 36 ++++++++++++++++++---- 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 731feef8586..5bca85289ca 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -368,11 +368,34 @@ module Json = struct ) objs + let of_api_message_or_error info = + let snake_to_camel (s : string) : string = + String.split_on_char '_' s + |> List.map (fun seg -> + let lower = String.lowercase_ascii seg in + match lower with + | "vm" + | "cpu" + | "tls" + | "xml" + | "url" + | "id" + | "uuid" + | "ip" + | "api" + | "eof" -> + String.uppercase_ascii lower + | _ -> + String.capitalize_ascii lower + ) + |> String.concat "" + in + `O [("name", `String (snake_to_camel info)); ("value", `String info)] + let api_messages = - List.map (fun (msg, _) -> `O [("name", `String msg)]) !Api_messages.msgList + List.map (fun (msg, _) -> of_api_message_or_error msg) !Api_messages.msgList - let api_errors = - List.map (fun error -> `O [("name", `String error)]) !Api_errors.errors + let api_errors = List.map of_api_message_or_error !Api_errors.errors end module Convert = struct diff --git a/ocaml/sdk-gen/go/test_data/api_errors.go b/ocaml/sdk-gen/go/test_data/api_errors.go index b14349c1885..1e6d67aba8a 100644 --- a/ocaml/sdk-gen/go/test_data/api_errors.go +++ b/ocaml/sdk-gen/go/test_data/api_errors.go @@ -1,6 +1,7 @@ +//nolint:gosec const ( // - ERR_MESSAGE_DEPRECATED = "MESSAGE_DEPRECATED" + ErrorMessageDeprecated = "MESSAGE_DEPRECATED" // - ERR_MESSAGE_REMOVED = "MESSAGE_REMOVED" -) + ErrorMessageRemoved = "MESSAGE_REMOVED" +) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/api_messages.go b/ocaml/sdk-gen/go/test_data/api_messages.go index f91592ec010..7f54389eb67 100644 --- a/ocaml/sdk-gen/go/test_data/api_messages.go +++ b/ocaml/sdk-gen/go/test_data/api_messages.go @@ -1,6 +1,6 @@ const ( // - MSG_HA_STATEFILE_LOST = "HA_STATEFILE_LOST" + MessageHaStatefileLost = "HA_STATEFILE_LOST" // - MSG_METADATA_LUN_HEALTHY = "METADATA_LUN_HEALTHY" + MessageMetadataLunHealthy = "METADATA_LUN_HEALTHY" ) \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/record.go b/ocaml/sdk-gen/go/test_data/record.go index fd5908e19c0..c07e18b9fb9 100644 --- a/ocaml/sdk-gen/go/test_data/record.go +++ b/ocaml/sdk-gen/go/test_data/record.go @@ -8,14 +8,16 @@ type SessionRecord struct { type SessionRef string // A session -type SessionClass struct { - client *rpcClient - ref SessionRef +type Session struct { + APIVersion APIVersion + client *rpcClient + ref SessionRef + XAPIVersion string } -func NewSession(opts *ClientOpts) *SessionClass { - client := NewJsonRPCClient(opts) - var session SessionClass +func NewSession(opts *ClientOpts) *Session { + client := newJSONRPCClient(opts) + var session Session session.client = client return &session diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 7609452ecfa..b75f678e5a4 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -295,10 +295,18 @@ let verify_obj = function | _ -> false +let verify_msg_or_error_member = function + | "name", `String _ | "value", `String _ -> + true + | _ -> + false + +let keys_in_error_or_msg = ["name"; "value"] + let verify_msgs_or_errors lst = let verify_msg_or_error = function - | `O [("name", `String _)] -> - true + | `O members -> + schema_check keys_in_error_or_msg verify_msg_or_error_member members | _ -> false in @@ -547,8 +555,16 @@ let api_errors : Mustache.Json.t = ( "api_errors" , `A [ - `O [("name", `String "MESSAGE_DEPRECATED")] - ; `O [("name", `String "MESSAGE_REMOVED")] + `O + [ + ("name", `String "MessageDeprecated") + ; ("value", `String "MESSAGE_DEPRECATED") + ] + ; `O + [ + ("name", `String "MessageRemoved") + ; ("value", `String "MESSAGE_REMOVED") + ] ] ) ] @@ -559,8 +575,16 @@ let api_messages : Mustache.Json.t = ( "api_messages" , `A [ - `O [("name", `String "HA_STATEFILE_LOST")] - ; `O [("name", `String "METADATA_LUN_HEALTHY")] + `O + [ + ("name", `String "HaStatefileLost") + ; ("value", `String "HA_STATEFILE_LOST") + ] + ; `O + [ + ("name", `String "MetadataLunHealthy") + ; ("value", `String "METADATA_LUN_HEALTHY") + ] ] ) ] From 3527346ffc61602cf1abe67d0debf4cef04d10e2 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 21:37:31 +0800 Subject: [PATCH 35/99] CP-48855: add templates for option and APIVersions Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- .../sdk-gen/go/templates/APIVersions.mustache | 103 ++++++++++++++++++ ocaml/sdk-gen/go/templates/Option.mustache | 4 + 2 files changed, 107 insertions(+) create mode 100644 ocaml/sdk-gen/go/templates/APIVersions.mustache create mode 100644 ocaml/sdk-gen/go/templates/Option.mustache diff --git a/ocaml/sdk-gen/go/templates/APIVersions.mustache b/ocaml/sdk-gen/go/templates/APIVersions.mustache new file mode 100644 index 00000000000..6c25e5a7035 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/APIVersions.mustache @@ -0,0 +1,103 @@ +type APIVersion int + +const ( +{{#releases}} + // {{branding}} ({{code_name}}) + APIVersion{{version_major}}_{{version_minor}}{{#first}} APIVersion = iota + 1{{/first}} +{{/releases}} + APIVersionLatest APIVersion = {{latest_version_index}} + APIVersionUnknown APIVersion = 99 +) + +func (v APIVersion) String() string { + switch v { +{{#releases}} + case APIVersion{{version_major}}_{{version_minor}}: + return "{{version_major}}.{{version_minor}}" +{{/releases}} + case APIVersionUnknown: + return "Unknown" + default: + return "Unknown" + } +} + +var APIVersionMap = map[string]APIVersion{ +{{#releases}} + // + "APIVersion{{version_major}}_{{version_minor}}": APIVersion{{version_major}}_{{version_minor}}, +{{/releases}} + // + "APIVersionLatest": APIVersionLatest, + // + "APIVersionUnknown": APIVersionUnknown, +} + +func GetAPIVersion(major int, minor int) APIVersion { + versionName := fmt.Sprintf("APIVersion%d_%d", major, minor) + apiVersion, ok := APIVersionMap[versionName] + if !ok { + apiVersion = APIVersionUnknown + } + + return apiVersion +} + +func getPoolMaster(session *Session) (HostRef, error) { + var master HostRef + poolRefs, err := Pool.GetAll(session) + if err != nil { + return master, err + } + if len(poolRefs) > 0 { + poolRecord, err := Pool.GetRecord(session, poolRefs[0]) + if err != nil { + return master, err + } + return poolRecord.Master, nil + } + return master, errors.New("pool master not found") +} + +func setSessionDetails(session *Session) error { + err := setAPIVersion(session) + if err != nil { + return err + } + err = setXAPIVersion(session) + if err != nil { + return err + } + return nil +} + +func setAPIVersion(session *Session) error { + session.APIVersion = APIVersionUnknown + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + session.APIVersion = GetAPIVersion(hostRecord.APIVersionMajor, hostRecord.APIVersionMinor) + return nil +} + +func setXAPIVersion(session *Session) error { + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + version, ok := hostRecord.SoftwareVersion["xapi"] + if !ok { + return errors.New("xapi version not found") + } + session.XAPIVersion = version + return nil +} diff --git a/ocaml/sdk-gen/go/templates/Option.mustache b/ocaml/sdk-gen/go/templates/Option.mustache new file mode 100644 index 00000000000..5066597f853 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/Option.mustache @@ -0,0 +1,4 @@ +{{#option}} +type Option{{type_name_suffix}} *{{type}} + +{{/option}} \ No newline at end of file From 420c07656b9d87e7fe9a0bb03ce4bba52ddf0ed7 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:15:34 +0800 Subject: [PATCH 36/99] CP-48855: render options Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_binding.ml | 3 ++- ocaml/sdk-gen/go/gen_go_helper.ml | 27 ++++++++++++++++---- ocaml/sdk-gen/go/test_gen_go.ml | 41 ++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 83f181328b5..a97a91090cc 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -103,6 +103,7 @@ let main destdir = let header_rendered = render_template "FileHeader.mustache" obj ~newline:true () in + let options_rendered = render_template "Option.mustache" obj () in let record_rendered = render_template "Record.mustache" obj () in let methods_rendered = if name = "session" then @@ -111,7 +112,7 @@ let main destdir = render_template "Methods.mustache" obj () in let rendered = - let first_half = header_rendered ^ record_rendered in + let first_half = header_rendered ^ options_rendered ^ record_rendered in match methods_rendered with | "" -> first_half diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 5bca85289ca..5bf74d0864d 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -49,6 +49,8 @@ let generate_file ~rendered ~destdir ~output_file = ~finally:(fun () -> close_out out_chan) module Json = struct + open Xapi_stdext_std + type enum = (string * string) list module StringMap = Map.Make (String) @@ -115,7 +117,9 @@ module Json = struct | Record r -> (snake_to_camel r ^ "Record", StringMap.empty) | Option ty -> - string_of_ty_with_enums ty + let _, e = string_of_ty_with_enums ty in + let name = suffix_of_type ty in + ("Option" ^ name, e) let of_enum name vs = let name = snake_to_camel name in @@ -157,9 +161,7 @@ module Json = struct let modules_of_types types = let common = [`O [("name", `String "fmt"); ("sname", `Null)]] in - let items = - List.map modules_of_type types |> List.concat |> List.append common - in + let items = List.concat_map modules_of_type types |> List.append common in `O [("import", `Bool true); ("items", `A items)] let all_enums objs = @@ -341,13 +343,27 @@ module Json = struct ) obj.messages + let of_option ty = + let name, _ = string_of_ty_with_enums ty in + `O + [ + ("type", `String name); ("type_name_suffix", `String (suffix_of_type ty)) + ] + + let of_options types = + types + |> List.filter_map (function Option ty -> Some ty | _ -> None) + |> List.map of_option + let xenapi objs = List.map (fun obj -> let obj_name = snake_to_camel obj.name in let name_internal = String.uncapitalize_ascii obj_name in let fields = Datamodel_utils.fields_of_obj obj in - let types = List.map (fun field -> field.ty) fields in + let types = + List.map (fun field -> field.ty) fields |> Listext.List.setify + in let modules = match obj.messages with [] -> `Null | _ -> modules_of_types types in @@ -361,6 +377,7 @@ module Json = struct ) ; ("modules", modules) ; ("messages", `A (messages_of_obj obj)) + ; ("option", `A (of_options types)) ] in let assoc_list = base_assoc_list @ get_event_session_value obj.name in diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index b75f678e5a4..d8866f901c7 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -258,6 +258,20 @@ let verify_enums : Mustache.Json.t -> bool = function | _ -> false +let option_keys = ["type"; "type_name_suffix"] + +let verify_option_member = function + | "type", `String _ | "type_name_suffix", `String _ -> + true + | _ -> + false + +let verify_option = function + | `O members -> + schema_check option_keys verify_option_member members + | _ -> + false + (* obj *) let verify_obj_member = function | "name", `String _ | "description", `String _ | "name_internal", `String _ -> @@ -270,6 +284,8 @@ let verify_obj_member = function List.for_all verify_field fields | "messages", `A messages -> List.for_all verify_message messages + | "option", `A options -> + List.for_all verify_option options | "modules", `Null -> true | "modules", `O members -> @@ -287,6 +303,7 @@ let obj_keys = ; "modules" ; "event" ; "session" + ; "option" ] let verify_obj = function @@ -903,6 +920,21 @@ let messages : Mustache.Json.t = ) ] +let option = + `O + [ + ( "option" + , `A + [ + `O + [ + ("type", `String "string") + ; ("type_name_suffix", `String "String") + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -956,6 +988,8 @@ module TemplatesTest = Generic.MakeStateless (struct let option_convert_rendered = string_of_file "option_convert.go" + let option_rendered = "type OptionString *string" + let tests = `QuickAndAutoDocumented [ @@ -982,6 +1016,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) ; (("ConvertOption.mustache", option_convert), option_convert_rendered) + ; (("Option.mustache", option), option_rendered) ] end) @@ -1160,11 +1195,13 @@ module StringOfTyWithEnumsTest = struct let test_record () = let ty, enums = Json.string_of_ty_with_enums (Record "pool") in - verify "datetime" verify_record (ty, enums) + verify "record" verify_record (ty, enums) + + let verify_option (ty, enums) = ty = "OptionString" && enums = StringMap.empty let test_option () = let ty, enums = Json.string_of_ty_with_enums (Option String) in - verify "datetime" verify_string (ty, enums) + verify "option" verify_string (ty, enums) let verify_map (ty, enums) = ty = "map[int]UpdateSync" From 181d49dbbae3c6eb2d781316d8c8d3d6f4eef262 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:26:09 +0800 Subject: [PATCH 37/99] CP-48855: render APIVersion Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 32 ++++--- ocaml/sdk-gen/go/gen_go_binding.ml | 44 +++++++-- ocaml/sdk-gen/go/test_data/api_versions.go | 103 +++++++++++++++++++++ ocaml/sdk-gen/go/test_gen_go.ml | 82 +++++++++++++++- 4 files changed, 239 insertions(+), 22 deletions(-) create mode 100644 ocaml/sdk-gen/go/test_data/api_versions.go diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index 989448f49e2..cffa59b4276 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -308,24 +308,32 @@ let json_releases = in try index_rec 0 list with Not_found -> -1 in - let json_of_rel x = + let of_rel x = let y = version_index_of x unique_version_bumps + 1 in - `O - [ - ( "code_name" - , `String (match x.code_name with Some r -> r | None -> "") - ) - ; ("version_major", `Float (float_of_int x.version_major)) - ; ("version_minor", `Float (float_of_int x.version_minor)) - ; ("branding", `String x.branding) - ; ("version_index", `Float (float_of_int y)) - ] + [ + ("code_name", `String (Option.value x.code_name ~default:"")) + ; ("version_major", `Float (float_of_int x.version_major)) + ; ("version_minor", `Float (float_of_int x.version_minor)) + ; ("branding", `String x.branding) + ; ("version_index", `Float (float_of_int y)) + ] + in + let of_rels releases = + match releases with + | [] -> + `A [] + | head :: tail -> + let head' = `O (("first", `Bool true) :: of_rel head) in + let tail' = + List.map (fun rel -> `O (("first", `Bool false) :: of_rel rel)) tail + in + `A (head' :: tail') in `O [ ("API_VERSION_MAJOR", `Float (Int64.to_float Datamodel.api_version_major)) ; ("API_VERSION_MINOR", `Float (Int64.to_float Datamodel.api_version_minor)) - ; ("releases", `A (List.map (fun x -> json_of_rel x) unique_version_bumps)) + ; ("releases", of_rels unique_version_bumps) ; ( "latest_version_index" , `Float (float_of_int (List.length unique_version_bumps)) ) diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index a97a91090cc..0203e16966c 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -24,6 +24,30 @@ let render_enums enums destdir = let rendered = header ^ enums ^ "\n" in generate_file ~rendered ~destdir ~output_file:"enums.go" +let render_api_versions destdir = + let header_json = + let name s = `O [("name", `String s); ("sname", `Null)] in + `O + [ + ( "modules" + , `O + [ + ("import", `Bool true) + ; ("items", `A (List.map name ["errors"; "fmt"])) + ] + ) + ] + in + let rendered = + let header = + render_template "FileHeader.mustache" header_json ~newline:true () + in + header + ^ render_template "APIVersions.mustache" CommonFunctions.json_releases + ~newline:true () + in + generate_file ~rendered ~destdir ~output_file:"api_versions.go" + let render_api_messages_and_errors destdir = let obj = `O @@ -93,6 +117,7 @@ let render_converts destdir = generate_file ~rendered ~destdir ~output_file:"convert.go" let main destdir = + render_api_versions destdir ; render_api_messages_and_errors destdir ; let enums = Json.all_enums objects in render_enums enums destdir ; @@ -104,7 +129,9 @@ let main destdir = render_template "FileHeader.mustache" obj ~newline:true () in let options_rendered = render_template "Option.mustache" obj () in - let record_rendered = render_template "Record.mustache" obj () in + let record_rendered = + render_template "Record.mustache" obj () ~newline:true + in let methods_rendered = if name = "session" then render_template "SessionMethod.mustache" obj () @@ -112,15 +139,14 @@ let main destdir = render_template "Methods.mustache" obj () in let rendered = - let first_half = header_rendered ^ options_rendered ^ record_rendered in - match methods_rendered with - | "" -> - first_half - | _ -> - first_half ^ "\n" ^ methods_rendered + let rendered = + [header_rendered; options_rendered; record_rendered; methods_rendered] + |> String.concat "" + |> String.trim + in + rendered ^ "\n" in - let output_file = name ^ ".go" in - generate_file ~rendered ~destdir ~output_file + generate_file ~rendered ~destdir ~output_file:(name ^ ".go") ) objects diff --git a/ocaml/sdk-gen/go/test_data/api_versions.go b/ocaml/sdk-gen/go/test_data/api_versions.go new file mode 100644 index 00000000000..b82411d5413 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/api_versions.go @@ -0,0 +1,103 @@ +type APIVersion int + +const ( + // XenServer 4.0 (rio) + APIVersion1_1 APIVersion = iota + 1 + // XenServer 4.1 (miami) + APIVersion1_2 + APIVersionLatest APIVersion = 2 + APIVersionUnknown APIVersion = 99 +) + +func (v APIVersion) String() string { + switch v { + case APIVersion1_1: + return "1.1" + case APIVersion1_2: + return "1.2" + case APIVersionUnknown: + return "Unknown" + default: + return "Unknown" + } +} + +var APIVersionMap = map[string]APIVersion{ + // + "APIVersion1_1": APIVersion1_1, + // + "APIVersion1_2": APIVersion1_2, + // + "APIVersionLatest": APIVersionLatest, + // + "APIVersionUnknown": APIVersionUnknown, +} + +func GetAPIVersion(major int, minor int) APIVersion { + versionName := fmt.Sprintf("APIVersion%d_%d", major, minor) + apiVersion, ok := APIVersionMap[versionName] + if !ok { + apiVersion = APIVersionUnknown + } + + return apiVersion +} + +func getPoolMaster(session *Session) (HostRef, error) { + var master HostRef + poolRefs, err := Pool.GetAll(session) + if err != nil { + return master, err + } + if len(poolRefs) > 0 { + poolRecord, err := Pool.GetRecord(session, poolRefs[0]) + if err != nil { + return master, err + } + return poolRecord.Master, nil + } + return master, errors.New("pool master not found") +} + +func setSessionDetails(session *Session) error { + err := setAPIVersion(session) + if err != nil { + return err + } + err = setXAPIVersion(session) + if err != nil { + return err + } + return nil +} + +func setAPIVersion(session *Session) error { + session.APIVersion = APIVersionUnknown + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + session.APIVersion = GetAPIVersion(hostRecord.APIVersionMajor, hostRecord.APIVersionMinor) + return nil +} + +func setXAPIVersion(session *Session) error { + masterRef, err := getPoolMaster(session) + if err != nil { + return err + } + hostRecord, err := Host.GetRecord(session, masterRef) + if err != nil { + return err + } + version, ok := hostRecord.SoftwareVersion["xapi"] + if !ok { + return errors.New("xapi version not found") + } + session.XAPIVersion = version + return nil +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index d8866f901c7..39e05547f26 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -335,6 +335,18 @@ let verify_simple_convert_member = function | _ -> false +let verify_release_member = function + | "branding", `String _ | "code_name", `String _ -> + true + | "first", `Bool _ -> + true + | "version_index", `Float _ + | "version_major", `Float _ + | "version_minor", `Float _ -> + true + | _ -> + false + let verify_simple_convert_keys = ["func_name_suffix"; "type"] let verify_simple_convert = function @@ -455,6 +467,41 @@ let verify_map_convert = function | _ -> false +let release_keys = + [ + "branding" + ; "code_name" + ; "version_major" + ; "version_minor" + ; "first" + ; "version_index" + ] + +let verify_release = function + | `O members -> + schema_check release_keys verify_release_member members + | _ -> + false + +let version_keys = + ["API_VERSION_MAJOR"; "API_VERSION_MINOR"; "latest_version_index"; "releases"] + +let verify_version_member = function + | "latest_version_index", `Float _ + | "API_VERSION_MAJOR", `Float _ + | "API_VERSION_MINOR", `Float _ -> + true + | "releases", `A releases -> + List.for_all verify_release releases + | _ -> + false + +let verify_version = function + | `O members -> + schema_check version_keys verify_version_member members + | _ -> + false + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -920,6 +967,33 @@ let messages : Mustache.Json.t = ) ] +let api_versions : Mustache.Json.t = + `O + [ + ("latest_version_index", `Float 2.) + ; ( "releases" + , `A + [ + `O + [ + ("branding", `String "XenServer 4.0") + ; ("code_name", `String "rio") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 1.) + ; ("first", `Bool true) + ] + ; `O + [ + ("branding", `String "XenServer 4.1") + ; ("code_name", `String "miami") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 2.) + ; ("first", `Bool false) + ] + ] + ) + ] + let option = `O [ @@ -988,6 +1062,8 @@ module TemplatesTest = Generic.MakeStateless (struct let option_convert_rendered = string_of_file "option_convert.go" + let api_versions_rendered = string_of_file "api_versions.go" + let option_rendered = "type OptionString *string" let tests = @@ -1016,6 +1092,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) ; (("ConvertOption.mustache", option_convert), option_convert_rendered) + ; (("APIVersions.mustache", api_versions), api_versions_rendered) ; (("Option.mustache", option), option_rendered) ] end) @@ -1036,11 +1113,14 @@ module TestGeneratedJson = struct verify "errors_and_msgs" verify_msgs_or_errors (Json.api_errors @ Json.api_messages) + let test_versions () = verify "versions" verify_version json_releases + let tests = [ ("enums", `Quick, test_enums) ; ("objs", `Quick, test_obj) ; ("errors_and_msgs", `Quick, test_errors_and_msgs) + ; ("versions", `Quick, test_versions) ] end @@ -1201,7 +1281,7 @@ module StringOfTyWithEnumsTest = struct let test_option () = let ty, enums = Json.string_of_ty_with_enums (Option String) in - verify "option" verify_string (ty, enums) + verify "option" verify_option (ty, enums) let verify_map (ty, enums) = ty = "map[int]UpdateSync" From e2a4919c5acbf1edd22078a4f7e30d246b5f2f6d Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Sat, 11 May 2024 14:00:10 +0800 Subject: [PATCH 38/99] CP-48855: remove go lint var-naming warnings Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 49 +++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 5bf74d0864d..0a8d3740b92 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -15,16 +15,41 @@ open Datamodel_types open CommonFunctions +module Types = Datamodel_utils.Types +module StringSet = Set.Make (String) let templates_dir = "templates" let ( // ) = Filename.concat +let acronyms = + [ + "id" + ; "ip" + ; "vm" + ; "api" + ; "uuid" + ; "cpu" + ; "tls" + ; "https" + ; "url" + ; "db" + ; "xml" + ; "eof" + ] + |> StringSet.of_list + +let is_acronym word = StringSet.mem word acronyms + let snake_to_camel (s : string) : string = Astring.String.cuts ~sep:"_" s - |> List.map (fun s -> Astring.String.cuts ~sep:"-" s) - |> List.concat - |> List.map String.capitalize_ascii + |> List.concat_map (fun s -> Astring.String.cuts ~sep:"-" s) + |> List.map (function + | s when is_acronym s -> + String.uppercase_ascii s + | s -> + String.capitalize_ascii s + ) |> String.concat "" let records = @@ -386,28 +411,22 @@ module Json = struct objs let of_api_message_or_error info = - let snake_to_camel (s : string) : string = + let xapi_constants_renaming (s : string) : string = String.split_on_char '_' s |> List.map (fun seg -> let lower = String.lowercase_ascii seg in match lower with - | "vm" - | "cpu" - | "tls" - | "xml" - | "url" - | "id" - | "uuid" - | "ip" - | "api" - | "eof" -> + | s when is_acronym s -> String.uppercase_ascii lower | _ -> String.capitalize_ascii lower ) |> String.concat "" in - `O [("name", `String (snake_to_camel info)); ("value", `String info)] + `O + [ + ("name", `String (xapi_constants_renaming info)); ("value", `String info) + ] let api_messages = List.map (fun (msg, _) -> of_api_message_or_error msg) !Api_messages.msgList From bc18f0ea81a1e84200e6c6b38b1242c2af7d3076 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Sat, 11 May 2024 12:48:46 +0800 Subject: [PATCH 39/99] CP-47356: Support backwards capability for Go SDK Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 216 ++++++++++------ ocaml/sdk-gen/go/gen_go_helper.mli | 3 + ocaml/sdk-gen/go/templates/Methods.mustache | 2 + .../go/templates/SessionMethod.mustache | 2 + ocaml/sdk-gen/go/test_data/methods.go | 2 + ocaml/sdk-gen/go/test_data/session_method.go | 2 + ocaml/sdk-gen/go/test_gen_go.ml | 232 +++++++++++++++++- 7 files changed, 383 insertions(+), 76 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 4a9623550a9..7bc83de9c75 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -242,40 +242,95 @@ module Json = struct ; ("func_name_suffix", `String (suffix_of_type t)) ] - let of_params params = - let name_internal name = - let name = name |> snake_to_camel |> String.uncapitalize_ascii in - match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name + let group_params msg = + let published_release releases = + match (published_release_for_param releases, releases) with + | "", [rel] -> + rel + | rel, _ -> + rel in - let of_param param = - let suffix_of_type = suffix_of_type param.param_type in - let t, _e = string_of_ty_with_enums param.param_type in - let name = param.param_name in - [ - ("is_session_id", `Bool (name = "session_id")) - ; ("type", `String t) - ; ("name", `String name) - ; ("name_internal", `String (name_internal name)) - ; ("doc", `String param.param_doc) - ; ("func_name_suffix", `String suffix_of_type) - ] + let do_group params = + let params = + List.map + (fun p -> (published_release p.param_release.internal, p)) + params + in + let uniq_sorted_rels = + List.map fst params + |> Listext.List.setify + |> List.fast_sort compare_versions + in + List.map + (fun rel -> + let params = + List.filter_map + (fun (r, param) -> + match compare_versions r rel with + | n when n > 0 -> + None + | _ -> + Some param + ) + params + in + (params, rel) + ) + uniq_sorted_rels + in + let only_session_group = + [([session_id], published_release msg.msg_release.internal)] in - (* We use ',' to seprate params in Go function, we should ignore ',' before first param, - for example `func(a type1, b type2)` is wanted rather than `func(, a type1, b type2)`. + let groups = + match (msg.msg_session, do_group msg.msg_params) with + | true, [] -> + only_session_group + | true, groups -> + List.map (fun (params, rel) -> (session_id :: params, rel)) groups + | false, groups -> + groups + in + (* The bool label in the tuple below is tagged to distinguish whether it is the latest parameters group. + If it's the latest parameters group, then we should not add the number of parameters to the method's name. *) - let add_first = function - | head :: rest -> - let head = `O (("first", `Bool true) :: of_param head) in - let rest = - List.map - (fun item -> `O (("first", `Bool false) :: of_param item)) - rest - in - head :: rest - | [] -> - [] + match List.rev groups with + | (params, rel) :: _ as groups -> + (true, params, rel) + :: List.map (fun (params, rel) -> (false, params, rel)) groups + | [] -> + failwith "Empty params group should not exist." + + let of_param param = + let name_internal name = + let name = name |> snake_to_camel |> String.uncapitalize_ascii in + match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name in - `A (add_first params) + let suffix_of_type = suffix_of_type param.param_type in + let t, _e = string_of_ty_with_enums param.param_type in + let name = param.param_name in + [ + ("is_session_id", `Bool (name = "session_id")) + ; ("type", `String t) + ; ("name", `String name) + ; ("name_internal", `String (name_internal name)) + ; ("doc", `String param.param_doc) + ; ("func_name_suffix", `String suffix_of_type) + ] + + (* We use ',' to seprate params in Go function, we should ignore ',' before first param, + for example `func(a type1, b type2)` is wanted rather than `func(, a type1, b type2)`. + *) + let of_params = function + | head :: rest -> + let head = `O (("first", `Bool true) :: of_param head) in + let rest = + List.map + (fun item -> `O (("first", `Bool false) :: of_param item)) + rest + in + head :: rest + | [] -> + [] let of_error e = `O [("name", `String e.err_name); ("doc", `String e.err_doc)] @@ -285,16 +340,6 @@ module Json = struct | errors -> `A (List.map of_error errors) - let add_session_info class_name method_name = - match (class_name, method_name) with - | "session", "login_with_password" - | "session", "slave_local_login_with_password" -> - [("session_login", `Bool true); ("session_logout", `Bool false)] - | "session", "logout" | "session", "local_logout" -> - [("session_login", `Bool false); ("session_logout", `Bool true)] - | _ -> - [("session_login", `Bool false); ("session_logout", `Bool false)] - let desc_of_msg msg ctor_fields = let ctor = if msg.msg_tag = FromObject Make then @@ -323,42 +368,65 @@ module Json = struct ) |> String.concat ", " + let method_name_exported method_name params latest = + let method_name = snake_to_camel method_name in + if latest then + method_name + else + method_name ^ string_of_int (List.length params) + + (* Since the param of `session *Session` isn't needed in functions of session object, + we add a special "func_params" field for session object to ignore `session *Session`.*) + let addtion_info_of_session method_name params latest = + let add_session_info method_name = + match method_name with + | "login_with_password" | "slave_local_login_with_password" -> + [("session_login", `Bool true); ("session_logout", `Bool false)] + | "logout" | "local_logout" -> + [("session_login", `Bool false); ("session_logout", `Bool true)] + | _ -> + [("session_login", `Bool false); ("session_logout", `Bool false)] + in + let name = method_name_exported method_name params latest in + ("func_params", `A (of_params params)) + :: ("method_name_exported", `String name) + :: add_session_info method_name + + let addtion_info msg params latest = + let method_name = msg.msg_name in + match (String.lowercase_ascii msg.msg_obj_name, msg.msg_session) with + | "session", true -> + addtion_info_of_session method_name (List.tl params) latest + | "session", false -> + addtion_info_of_session method_name params latest + | _ -> + let name = method_name_exported method_name params latest in + [("method_name_exported", `String name)] + let messages_of_obj obj = let ctor_fields = ctor_fields_of_obj obj in - let params_in_msg msg = - if msg.msg_session then - session_id :: msg.msg_params - else - msg.msg_params - in - List.map - (fun msg -> - let params = params_in_msg msg |> of_params in - let base_assoc_list = - [ - ("method_name", `String msg.msg_name) - ; ("class_name", `String obj.name) - ; ("class_name_exported", `String (snake_to_camel obj.name)) - ; ("method_name_exported", `String (snake_to_camel msg.msg_name)) - ; ("description", desc_of_msg msg ctor_fields) - ; ("result", of_result obj msg) - ; ("params", params) - ; ("errors", of_errors msg.msg_errors) - ; ("has_error", `Bool (msg.msg_errors <> [])) - ; ("async", `Bool msg.msg_async) - ] - in - (* Since the param of `session *Session` isn't needed in functions of session object, - we add a special "func_params" field for session object to ignore `session *Session`.*) - if obj.name = "session" then - `O - (("func_params", msg.msg_params |> of_params) - :: (add_session_info obj.name msg.msg_name @ base_assoc_list) - ) - else - `O base_assoc_list - ) - obj.messages + obj.messages + |> List.rev_map (fun msg -> + let of_message (latest, params, rel_version) = + let base_assoc_list = + [ + ("method_name", `String msg.msg_name) + ; ("class_name", `String obj.name) + ; ("class_name_exported", `String (snake_to_camel obj.name)) + ; ("description", desc_of_msg msg ctor_fields) + ; ("result", of_result obj msg) + ; ("params", `A (of_params params)) + ; ("errors", of_errors msg.msg_errors) + ; ("has_error", `Bool (msg.msg_errors <> [])) + ; ("async", `Bool msg.msg_async) + ; ("version", `String rel_version) + ] + in + `O (base_assoc_list @ addtion_info msg params latest) + in + msg |> group_params |> List.map of_message + ) + |> List.concat let of_option ty = let name, _ = string_of_ty_with_enums ty in diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 85dff4ba4d4..d5de1050a05 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -32,6 +32,9 @@ module Json : sig val string_of_ty_with_enums : Datamodel_types.ty -> string * enums + val group_params : + Datamodel_types.message -> (bool * Datamodel_types.param list * string) list + val xenapi : Datamodel_types.obj list -> (string * Mustache.Json.t) list val all_enums : Datamodel_types.obj list -> Mustache.Json.t diff --git a/ocaml/sdk-gen/go/templates/Methods.mustache b/ocaml/sdk-gen/go/templates/Methods.mustache index 32a0a5c8982..384b6949499 100644 --- a/ocaml/sdk-gen/go/templates/Methods.mustache +++ b/ocaml/sdk-gen/go/templates/Methods.mustache @@ -1,5 +1,6 @@ {{#messages}} // {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#version}}// Version: {{.}}{{/version}} {{#has_error}} // // Errors: @@ -27,6 +28,7 @@ func ({{name_internal}}) {{method_name_exported}}({{#params}}{{#first}}session * {{#async}} // Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#version}}// Version: {{.}}{{/version}} {{#has_error}} // // Errors: diff --git a/ocaml/sdk-gen/go/templates/SessionMethod.mustache b/ocaml/sdk-gen/go/templates/SessionMethod.mustache index b73fb057379..7613c69e75f 100644 --- a/ocaml/sdk-gen/go/templates/SessionMethod.mustache +++ b/ocaml/sdk-gen/go/templates/SessionMethod.mustache @@ -1,5 +1,6 @@ {{#messages}} // {{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#version}}// Version: {{.}}{{/version}} {{#has_error}} // // Errors: @@ -37,6 +38,7 @@ func (class *Session) {{method_name_exported}}({{#func_params}}{{^first}}, {{/fi {{#async}} // Async{{method_name_exported}}:{{#description}} {{.}}{{/description}} +{{#version}}// Version: {{.}}{{/version}} {{#has_error}} // // Errors: diff --git a/ocaml/sdk-gen/go/test_data/methods.go b/ocaml/sdk-gen/go/test_data/methods.go index 3e80d71b0f7..fd268ba83bb 100644 --- a/ocaml/sdk-gen/go/test_data/methods.go +++ b/ocaml/sdk-gen/go/test_data/methods.go @@ -1,4 +1,5 @@ // GetLog: GetLog Get the host log file +// Version: miami func (host) GetLog(session *Session, host HostRef) (retval string, err error) { method := "host.get_log" sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) @@ -18,6 +19,7 @@ func (host) GetLog(session *Session, host HostRef) (retval string, err error) { } // AsyncGetLog: GetLog Get the host log file +// Version: miami func (host) AsyncGetLog(session *Session, host HostRef) (retval TaskRef, err error) { method := "Async.host.get_log" sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), session.ref) diff --git a/ocaml/sdk-gen/go/test_data/session_method.go b/ocaml/sdk-gen/go/test_data/session_method.go index b476f4606a8..cda84de9ad6 100644 --- a/ocaml/sdk-gen/go/test_data/session_method.go +++ b/ocaml/sdk-gen/go/test_data/session_method.go @@ -1,4 +1,5 @@ // LoginWithPassword: Attempt to authenticate the user); returning a session reference if successful +// Version: miami // // Errors: // SESSION_AUTHENTICATION_FAILED - The credentials given by the user are incorrect @@ -26,6 +27,7 @@ func (class *Session) LoginWithPassword(uname string, pwd string) (retval Sessio } // Logout: Logout Log out of a session +// Version: miami func (class *Session) Logout() (err error) { method := "session.logout" sessionIDArg, err := serializeSessionRef(fmt.Sprintf("%s(%s)", method, "session_id"), class.ref) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 4767b0c05ec..ef484cf0622 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -120,7 +120,8 @@ let verify_message_member = function | "method_name", `String _ | "class_name", `String _ | "class_name_exported", `String _ - | "method_name_exported", `String _ -> + | "method_name_exported", `String _ + | "version", `String _ -> true | "description", `String _ | "description", `Null -> true @@ -141,7 +142,8 @@ let verify_sesseion_message_member = function | "method_name", `String _ | "class_name", `String _ | "class_name_exported", `String _ - | "method_name_exported", `String _ -> + | "method_name_exported", `String _ + | "version", `String _ -> true | "description", `String _ | "description", `Null -> true @@ -176,6 +178,7 @@ let message_keys = ; "errors" ; "has_error" ; "async" + ; "version" ] let session_message_keys = @@ -589,6 +592,7 @@ let session_messages : Mustache.Json.t = reference if successful" ) ; ("async", `Bool false) + ; ("version", `String "miami") ; ( "func_params" , `A [ @@ -666,6 +670,7 @@ let session_messages : Mustache.Json.t = ; ("description", `String "Logout Log out of a session") ; ("async", `Bool false) ; ("func_params", `A []) + ; ("version", `String "miami") ; ( "params" , `A [ @@ -701,6 +706,7 @@ let messages : Mustache.Json.t = ; ("method_name_exported", `String "GetLog") ; ("description", `String "GetLog Get the host log file") ; ("async", `Bool true) + ; ("version", `String "miami") ; ( "params" , `A [ @@ -934,6 +940,227 @@ module StringOfTyWithEnumsTest = struct ] end +module GroupParamsTest = Generic.MakeStateless (struct + open Datamodel_types + open Datamodel_common + + let string_of_message msg = msg.msg_obj_name ^ "." ^ msg.msg_name + + let string_of_param param = param.param_name ^ ":" ^ param.param_doc + + let string_of_group (latest, params, rel_version) = + Printf.sprintf "(latest = %b, release_version = %s, params = [%s])" latest + rel_version + (Test_printers.list string_of_param params) + + let string_of_groups groups = + Printf.sprintf "param_groups : [%s]}" + (Test_printers.list string_of_group groups) + + module Io = struct + type input_t = message + + type output_t = ((bool * param list * string) list, string) result + + let string_of_input_t = string_of_message + + let string_of_output_t = function + | Ok groups -> + Fmt.(str "%a" Dump.string) (string_of_groups groups) + | Error e -> + Fmt.(str "%a" Dump.string) e + end + + let transform message = + try Ok (Json.group_params message) with Failure e -> Error e + + let network = + { + param_type= Ref _network + ; param_name= "network" + ; param_doc= "Network to add the bonded PIF to" + ; param_release= miami_release + ; param_default= None + } + + let members = + { + param_type= Set (Ref _pif) + ; param_name= "members" + ; param_doc= "PIFs to add to this bond" + ; param_release= miami_release + ; param_default= None + } + + let mac = + { + param_type= String + ; param_name= "MAC" + ; param_doc= "The MAC address to use on the bond itself." + ; param_release= miami_release + ; param_default= None + } + + let mode = + { + param_type= Enum ("bond_mode", [("balance-slb", "Source-level balancing")]) + ; param_name= "mode" + ; param_doc= "Bonding mode to use for the new bond" + ; param_release= boston_release + ; param_default= Some (VEnum "balance-slb") + } + + let properties = + { + param_type= Map (String, String) + ; param_name= "properties" + ; param_doc= "Additional configuration parameters specific to the bond mode" + ; param_release= tampa_release + ; param_default= Some (VMap []) + } + + let num_release = numbered_release "1.250.0" + + let numbered_release_param = + { + param_type= String + ; param_name= "param" + ; param_doc= "A parm for testing" + ; param_release= num_release + ; param_default= None + } + + let group1 = [network; members; mac] + + let group2 = group1 @ [mode] + + let group3 = group2 @ [properties] + + let group4 = group3 @ [numbered_release_param] + + let msg_with_session = + { + msg_name= "create" + ; msg_params= group4 + ; msg_result= Some (Ref "Bond", "The reference of the created Bond object") + ; msg_errors= [] + ; msg_doc= "Create an interface bond" + ; msg_async= true + ; msg_session= true + ; msg_secret= false + ; msg_pool_internal= false + ; msg_db_only= false + ; msg_release= miami_release + ; msg_lifecycle= Lifecycle.from [] + ; msg_has_effect= true + ; msg_force_custom= None + ; msg_no_current_operations= false + ; msg_tag= Custom + ; msg_obj_name= "Bond" + ; msg_custom_marshaller= false + ; msg_hide_from_docs= false + ; msg_allowed_roles= Some ["pool-admin"; "pool-operator"] + ; msg_map_keys_roles= [] + ; msg_doc_tags= [] + ; msg_forward_to= None + } + + let num_version = published_release_for_param num_release.internal + + let msg_with_session_expected = + [ + (true, session_id :: group4, num_version) + ; (false, session_id :: group4, num_version) + ; (false, session_id :: group3, rel_tampa) + ; (false, session_id :: group2, rel_boston) + ; (false, session_id :: group1, rel_miami) + ] + + let msg_with_session_with_only_param = + {msg_with_session with msg_params= [network]} + + let msg_with_session_with_only_param_expected = + [ + (true, [session_id; network], rel_miami) + ; (false, [session_id; network], rel_miami) + ] + + let msg_without_session = {msg_with_session with msg_session= false} + + let msg_without_session_expected = + [ + (true, group4, num_version) + ; (false, group4, num_version) + ; (false, group3, rel_tampa) + ; (false, group2, rel_boston) + ; (false, group1, rel_miami) + ] + + let msg_without_session_with_only_param = + {msg_with_session with msg_session= false; msg_params= [network]} + + let msg_without_session_with_only_param_expected = + [(true, [network], rel_miami); (false, [network], rel_miami)] + + (*Message has session param, but has no other params.*) + let msg_with_session_without_param = {msg_with_session with msg_params= []} + + let msg_with_session_without_param_expected = + let version = + published_release_for_param + msg_with_session_without_param.msg_release.internal + in + [(true, [session_id], version); (false, [session_id], version)] + + let msg_without_session_without_param = + {msg_with_session with msg_params= []; msg_session= false} + + (*Message which in session object has not session param and has no other params.*) + let msg_with_session_without_param_in_session_object = + {msg_with_session with msg_params= []; msg_obj_name= "Session"} + + let msg_with_session_without_param_in_session_object_expected = + let version = + published_release_for_param + msg_with_session_without_param_in_session_object.msg_release.internal + in + [(true, [session_id], version); (false, [session_id], version)] + + (*Message which in session object has not session param and has no other params.*) + let msg_without_session_without_param_in_session_object = + { + msg_with_session with + msg_params= [] + ; msg_obj_name= "Session" + ; msg_session= false + } + + let tests = + `QuickAndAutoDocumented + [ + (msg_with_session, Ok msg_with_session_expected) + ; ( msg_with_session_with_only_param + , Ok msg_with_session_with_only_param_expected + ) + ; (msg_without_session, Ok msg_without_session_expected) + ; ( msg_without_session_with_only_param + , Ok msg_without_session_with_only_param_expected + ) + ; ( msg_with_session_without_param + , Ok msg_with_session_without_param_expected + ) + ; ( msg_without_session_without_param + , Error "Empty params group should not exist." + ) + ; ( msg_with_session_without_param_in_session_object + , Ok msg_with_session_without_param_in_session_object_expected + ) + ; ( msg_without_session_without_param_in_session_object + , Error "Empty params group should not exist." + ) + ] +end) + let tests = make_suite "gen_go_binding_" [ @@ -942,6 +1169,7 @@ let tests = ; ("string_of_ty_with_enums", StringOfTyWithEnumsTest.tests) ; ("templates", TemplatesTest.tests) ; ("generated_mustache_jsons", TestGeneratedJson.tests) + ; ("group_params", GroupParamsTest.tests) ] let () = Alcotest.run "Gen go binding" tests From ed99de9f128cc881bd9ba9d7f000b13a56c202b9 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 13:56:15 +0800 Subject: [PATCH 40/99] CP-47361: generate mustache template for deserialize and serialize functions Signed-off-by: xueqingz Signed-off-by: Luca Zhang --- .../go/templates/ConvertBatch.mustache | 20 +++++++ .../sdk-gen/go/templates/ConvertEnum.mustache | 25 +++++++++ .../go/templates/ConvertFloat.mustache | 38 ++++++++++++++ .../sdk-gen/go/templates/ConvertInt.mustache | 25 +++++++++ .../go/templates/ConvertInterface.mustache | 8 +++ .../sdk-gen/go/templates/ConvertMap.mustache | 43 +++++++++++++++ .../go/templates/ConvertOption.mustache | 27 ++++++++++ .../go/templates/ConvertRecord.mustache | 52 +++++++++++++++++++ .../sdk-gen/go/templates/ConvertRef.mustache | 18 +++++++ .../sdk-gen/go/templates/ConvertSet.mustache | 35 +++++++++++++ .../go/templates/ConvertSimpleType.mustache | 20 +++++++ .../sdk-gen/go/templates/ConvertTime.mustache | 34 ++++++++++++ 12 files changed, 345 insertions(+) create mode 100644 ocaml/sdk-gen/go/templates/ConvertBatch.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertEnum.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertFloat.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertInt.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertInterface.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertMap.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertOption.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertRecord.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertRef.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertSet.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache create mode 100644 ocaml/sdk-gen/go/templates/ConvertTime.mustache diff --git a/ocaml/sdk-gen/go/templates/ConvertBatch.mustache b/ocaml/sdk-gen/go/templates/ConvertBatch.mustache new file mode 100644 index 00000000000..42f05791cb8 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertBatch.mustache @@ -0,0 +1,20 @@ +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (batch {{type}}, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } +{{#elements}} + {{name_internal}}Value, ok := rpcStruct["{{name}}"] + if ok && {{name_internal}}Value != nil { + batch.{{name_exported}}, err = deserialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), {{name_internal}}Value) + if err != nil { + return + } + } +{{/elements}} + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertEnum.mustache b/ocaml/sdk-gen/go/templates/ConvertEnum.mustache new file mode 100644 index 00000000000..85bb1660c24 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertEnum.mustache @@ -0,0 +1,25 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) (string, error) { + _ = context + return string(value), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + strValue, err := deserializeString(context, input) + if err != nil { + return + } + switch strValue { +{{#items}} + case "{{value}}": + value = {{name}} +{{/items}} + default: + err = fmt.Errorf("unable to parse XenAPI response: got value %q for enum %s at %s, but this is not any of the known values", strValue, "{{type}}", context) + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertFloat.mustache b/ocaml/sdk-gen/go/templates/ConvertFloat.mustache new file mode 100644 index 00000000000..89c5910909d --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertFloat.mustache @@ -0,0 +1,38 @@ +{{#serialize}} +//nolint:unparam +func serialize{{func_name_suffix}}(context string, value {{type}}) (interface{}, error) { + _ = context + if math.IsInf(value, 0) { + if math.IsInf(value, 1) { + return "+Inf", nil + } + return "-Inf", nil + } else if math.IsNaN(value) { + return "NaN", nil + } + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.ParseFloat(strValue, 64) + if err != nil { + switch strValue { + case "+Inf": + return math.Inf(1), nil + case "-Inf": + return math.Inf(-1), nil + case "NaN": + return math.NaN(), nil + } + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertInt.mustache b/ocaml/sdk-gen/go/templates/ConvertInt.mustache new file mode 100644 index 00000000000..dbc7cf37c56 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertInt.mustache @@ -0,0 +1,25 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) ({{type}}, error) { + _ = context + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.Atoi(strValue) + if err != nil { + floatValue, err1 := strconv.ParseFloat(strValue, 64) + if err1 == nil { + return int(floatValue), nil + } + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertInterface.mustache b/ocaml/sdk-gen/go/templates/ConvertInterface.mustache new file mode 100644 index 00000000000..9090d083058 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertInterface.mustache @@ -0,0 +1,8 @@ +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (inter {{type}}, err error) { + _ = context + inter = input + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertMap.mustache b/ocaml/sdk-gen/go/templates/ConvertMap.mustache new file mode 100644 index 00000000000..b4cca1d7ca8 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertMap.mustache @@ -0,0 +1,43 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, goMap {{type}}) (xenMap map[string]interface{}, err error) { + xenMap = make(map[string]interface{}) + for goKey, goValue := range goMap { + keyContext := fmt.Sprintf("%s[%s]", context, goKey) + xenKey, err := serialize{{key_type}}(keyContext, goKey) + if err != nil { + return xenMap, err + } + xenValue, err := serialize{{value_type}}(keyContext, goValue) + if err != nil { + return xenMap, err + } + xenMap[xenKey] = xenValue + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (goMap {{type}}, err error) { + xenMap, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + goMap = make({{type}}, len(xenMap)) + for xenKey, xenValue := range xenMap { + keyContext := fmt.Sprintf("%s[%s]", context, xenKey) + goKey, err := deserialize{{key_type}}(keyContext, xenKey) + if err != nil { + return goMap, err + } + goValue, err := deserialize{{value_type}}(keyContext, xenValue) + if err != nil { + return goMap, err + } + goMap[goKey] = goValue + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertOption.mustache b/ocaml/sdk-gen/go/templates/ConvertOption.mustache new file mode 100644 index 00000000000..ca49dea9f3b --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertOption.mustache @@ -0,0 +1,27 @@ +{{#serialize}} +func serializeOption{{func_name_suffix}}(context string, input Option{{func_name_suffix}}) (option interface{}, err error) { + if input == nil { + return + } + option, err = serialize{{func_name_suffix}}(context, *input) + if err != nil { + return + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserializeOption{{func_name_suffix}}(context string, input interface{}) (option Option{{func_name_suffix}}, err error) { + if input == nil { + return + } + value, err := deserialize{{func_name_suffix}}(context, input) + if err != nil { + return + } + option = &value + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertRecord.mustache b/ocaml/sdk-gen/go/templates/ConvertRecord.mustache new file mode 100644 index 00000000000..6e973b87dd0 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertRecord.mustache @@ -0,0 +1,52 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, record {{type}}) (rpcStruct map[string]interface{}, err error) { + rpcStruct = map[string]interface{}{} +{{#fields}} +{{#type_option}} + {{name_internal}}, err := serializeOption{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), record.{{name_exported}}) + if err != nil { + return + } + if {{name_internal}} != nil { + rpcStruct["{{name}}"] = {{name_internal}} + } +{{/type_option}} +{{^type_option}} + rpcStruct["{{name}}"], err = serialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), record.{{name_exported}}) + if err != nil { + return + } +{{/type_option}} +{{/fields}} + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (record {{type}}, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } +{{#fields}} +{{#type_option}} + record.{{name_exported}}, err = deserializeOption{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), rpcStruct["{{name}}"]) + if err != nil { + return + } +{{/type_option}} +{{^type_option}} + {{name_internal}}Value, ok := rpcStruct["{{name}}"] + if ok && {{name_internal}}Value != nil { + record.{{name_exported}}, err = deserialize{{func_name_suffix}}(fmt.Sprintf("%s.%s", context, "{{name}}"), {{name_internal}}Value) + if err != nil { + return + } + } +{{/type_option}} +{{/fields}} + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertRef.mustache b/ocaml/sdk-gen/go/templates/ConvertRef.mustache new file mode 100644 index 00000000000..2b938dcb658 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertRef.mustache @@ -0,0 +1,18 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, ref {{type}}) (string, error) { + _ = context + return string(ref), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) ({{type}}, error) { + var ref {{type}} + value, ok := input.(string) + if !ok { + return ref, fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return {{type}}(value), nil +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertSet.mustache b/ocaml/sdk-gen/go/templates/ConvertSet.mustache new file mode 100644 index 00000000000..c3f37099a78 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertSet.mustache @@ -0,0 +1,35 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, slice []{{type}}) (set []interface{}, err error) { + set = make([]interface{}, len(slice)) + for index, item := range slice { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := serialize{{item_func_suffix}}(itemContext, item) + if err != nil { + return set, err + } + set[index] = itemValue + } + return +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (slice []{{type}}, err error) { + set, ok := input.([]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "[]interface{}", context, reflect.TypeOf(input), input) + return + } + slice = make([]{{type}}, len(set)) + for index, item := range set { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := deserialize{{item_func_suffix}}(itemContext, item) + if err != nil { + return slice, err + } + slice[index] = itemValue + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache b/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache new file mode 100644 index 00000000000..552052932a6 --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertSimpleType.mustache @@ -0,0 +1,20 @@ +{{#serialize}} +func serialize{{func_name_suffix}}(context string, value {{type}}) ({{type}}, error) { + _ = context + return value, nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + if input == nil { + return + } + value, ok := input.({{type}}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "{{type}}", context, reflect.TypeOf(input), input) + } + return +} + +{{/deserialize}} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/templates/ConvertTime.mustache b/ocaml/sdk-gen/go/templates/ConvertTime.mustache new file mode 100644 index 00000000000..d79f65841ad --- /dev/null +++ b/ocaml/sdk-gen/go/templates/ConvertTime.mustache @@ -0,0 +1,34 @@ +{{#serialize}} +var timeFormats = []string{time.RFC3339, "20060102T15:04:05Z", "20060102T15:04:05"} + +//nolint:unparam +func serialize{{func_name_suffix}}(context string, value {{type}}) (string, error) { + _ = context + return value.Format(time.RFC3339), nil +} + +{{/serialize}} +{{#deserialize}} +func deserialize{{func_name_suffix}}(context string, input interface{}) (value {{type}}, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + floatValue, err := strconv.ParseFloat(strValue, 64) + if err != nil { + for _, timeFormat := range timeFormats { + value, err = time.Parse(timeFormat, strValue) + if err == nil { + return value, nil + } + } + return + } + unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) + value = time.Unix(unixTimestamp, 0) + + return +} + +{{/deserialize}} \ No newline at end of file From 90b9cfcb9d0dce3b3e9891a724257c8b62be6d58 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 14:20:01 +0800 Subject: [PATCH 41/99] CP-47358: Generate convert functions Go code Signed-off-by: Luca Zhang --- ocaml/sdk-gen/common/CommonFunctions.ml | 53 +++++ ocaml/sdk-gen/common/CommonFunctions.mli | 8 + ocaml/sdk-gen/go/gen_go_binding.ml | 49 ++++ ocaml/sdk-gen/go/gen_go_helper.ml | 277 ++++++++++++++++++++++- ocaml/sdk-gen/go/gen_go_helper.mli | 69 ++++++ 5 files changed, 455 insertions(+), 1 deletion(-) diff --git a/ocaml/sdk-gen/common/CommonFunctions.ml b/ocaml/sdk-gen/common/CommonFunctions.ml index b2db32f3c51..5f1b5b3a560 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.ml +++ b/ocaml/sdk-gen/common/CommonFunctions.ml @@ -361,3 +361,56 @@ let objects = api in objects_of_api api + +module TypesOfMessages = struct + open Xapi_stdext_std + + let records = + List.map + (fun obj -> + let obj_name = String.lowercase_ascii obj.name in + (obj_name, Datamodel_utils.fields_of_obj obj) + ) + objects + + let rec decompose = function + | Set x as y -> + y :: decompose x + | Map (a, b) as y -> + (y :: decompose a) @ decompose b + | Option x as y -> + y :: decompose x + | Record r as y -> + let name = String.lowercase_ascii r in + let types_in_field = + List.assoc_opt name records + |> Option.value ~default:[] + |> List.concat_map (fun field -> decompose field.ty) + in + y :: types_in_field + | (SecretString | String | Int | Float | DateTime | Enum _ | Bool | Ref _) + as x -> + [x] + + let mesages objects = objects |> List.concat_map (fun x -> x.messages) + + (** All types of params in a list of objects (automatically decomposes) *) + let of_params objects = + let param_types = + mesages objects + |> List.concat_map (fun x -> x.msg_params) + |> List.map (fun p -> p.param_type) + |> Listext.List.setify + in + List.concat_map decompose param_types |> Listext.List.setify + + (** All types of results in a list of objects (automatically decomposes) *) + let of_results objects = + let return_types = + let aux accu msg = + match msg.msg_result with None -> accu | Some (ty, _) -> ty :: accu + in + mesages objects |> List.fold_left aux [] |> Listext.List.setify + in + List.concat_map decompose return_types |> Listext.List.setify +end diff --git a/ocaml/sdk-gen/common/CommonFunctions.mli b/ocaml/sdk-gen/common/CommonFunctions.mli index c738f73b4a7..a927c5400ec 100644 --- a/ocaml/sdk-gen/common/CommonFunctions.mli +++ b/ocaml/sdk-gen/common/CommonFunctions.mli @@ -146,3 +146,11 @@ val compare_versions : string -> string -> int @param published release r2. @return the order diff between r1 and r2. *) + +module TypesOfMessages : sig + val of_params : Datamodel_types.obj list -> Datamodel_types.ty list + (** All the types in the params of messages*) + + val of_results : Datamodel_types.obj list -> Datamodel_types.ty list + (** All the types in the results of messages*) +end diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index b12077a7593..0203e16966c 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -68,11 +68,60 @@ let render_api_messages_and_errors destdir = generate_file ~rendered:messages_rendered ~destdir ~output_file:"api_messages.go" +let render_convert_header () = + let name s = `O [("name", `String s); ("sname", `Null)] in + let obj = + `O + [ + ("name", `String "convert") + ; ( "modules" + , `O + [ + ("import", `Bool true) + ; ( "items" + , `A (List.map name ["fmt"; "math"; "reflect"; "strconv"; "time"]) + ) + ] + ) + ] + in + render_template "FileHeader.mustache" obj ~newline:true () + +let render_converts destdir = + let event = render_template "ConvertBatch.mustache" Convert.event_batch () in + let interface = + render_template "ConvertInterface.mustache" Convert.interface () + in + let param_types = TypesOfMessages.of_params objects in + let result_types = TypesOfMessages.of_results objects in + let generate types of_json = + types + |> List.map (fun ty -> + let params = Convert.of_ty ty in + let template = Convert.template_of_convert params in + let json : Mustache.Json.t = of_json params in + render_template template json () + ) + |> String.concat "" + in + let rendered = + let serializes_rendered = generate param_types Convert.of_serialize in + let deserializes_rendered = generate result_types Convert.of_deserialize in + render_convert_header () + ^ serializes_rendered + ^ deserializes_rendered + ^ event + ^ String.trim interface + ^ "\n" + in + generate_file ~rendered ~destdir ~output_file:"convert.go" + let main destdir = render_api_versions destdir ; render_api_messages_and_errors destdir ; let enums = Json.all_enums objects in render_enums enums destdir ; + render_converts destdir ; let objects = Json.xenapi objects in List.iter (fun (name, obj) -> diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 7bc83de9c75..0f9760dbdab 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -52,6 +52,14 @@ let snake_to_camel (s : string) : string = ) |> String.concat "" +let records = + List.map + (fun obj -> + let obj_name = snake_to_camel obj.name ^ "Record" in + (obj_name, Datamodel_utils.fields_of_obj obj) + ) + objects + let render_template template_file json ?(newline = false) () = let templ = string_of_file (templates_dir // template_file) |> Mustache.of_string @@ -183,7 +191,7 @@ module Json = struct let all_enums objs = let enums = - Types.of_objects objs + Datamodel_utils.Types.of_objects objs |> List.map (fun ty -> let _, e = string_of_ty_with_enums ty in e @@ -493,3 +501,270 @@ module Json = struct let api_errors = List.map of_api_message_or_error !Api_errors.errors end + +module Convert = struct + type params = {func_suffix: string; value_ty: string} + + type params_of_option = {func_suffix: string} + + type params_of_set = { + func_suffix: string + ; value_ty: string + ; item_fp_type: string + } + + type params_of_record_field = { + name: string + ; name_internal: string + ; name_exported: string + ; func_suffix: string + ; type_option: bool + } + + type params_of_record = { + func_suffix: string + ; value_ty: string + ; fields: params_of_record_field list + } + + type params_of_enum_item = {value: string; name: string} + + type params_of_enum = { + func_suffix: string + ; value_ty: string + ; items: params_of_enum_item list + } + + type params_of_map = { + func_suffix: string + ; value_ty: string + ; key_ty: string + ; val_ty: string + } + + type convert_params = + | Simple of params + | Int of params + | Float of params + | Time of params + | Ref of params + | Option of params_of_option + | Set of params_of_set + | Enum of params_of_enum + | Record of params_of_record + | Map of params_of_map + + let template_of_convert : convert_params -> string = function + | Simple _ -> + "ConvertSimpleType.mustache" + | Int _ -> + "ConvertInt.mustache" + | Float _ -> + "ConvertFloat.mustache" + | Time _ -> + "ConvertTime.mustache" + | Ref _ -> + "ConvertRef.mustache" + | Set _ -> + "ConvertSet.mustache" + | Record _ -> + "ConvertRecord.mustache" + | Map _ -> + "ConvertMap.mustache" + | Enum _ -> + "ConvertEnum.mustache" + | Option _ -> + "ConvertOption.mustache" + + let to_json : convert_params -> Mustache.Json.value = function + | Simple params | Int params | Float params | Time params | Ref params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ] + | Option params -> + `O [("func_name_suffix", `String params.func_suffix)] + | Set params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("item_func_suffix", `String params.item_fp_type) + ] + | Record params -> + let fields = + List.rev_map + (fun (field : params_of_record_field) -> + `O + [ + ("name", `String field.name) + ; ("name_internal", `String field.name_internal) + ; ("name_exported", `String field.name_exported) + ; ("func_name_suffix", `String field.func_suffix) + ; ("type_option", `Bool field.type_option) + ] + ) + params.fields + in + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("fields", `A fields) + ] + | Enum params -> + let of_value item = + `O [("value", `String item.value); ("name", `String item.name)] + in + `O + [ + ("type", `String params.value_ty) + ; ("func_name_suffix", `String params.func_suffix) + ; ("items", `A (List.map of_value params.items)) + ] + | Map params -> + `O + [ + ("func_name_suffix", `String params.func_suffix) + ; ("type", `String params.value_ty) + ; ("key_type", `String params.key_ty) + ; ("value_type", `String params.val_ty) + ] + + let fields record_name = + let fields = + List.assoc_opt record_name records + |> Option.value ~default:[] + |> List.rev_map (fun field -> + ( String.concat "_" field.full_name + , Json.suffix_of_type field.ty + , match field.ty with Option _ -> true | _ -> false + ) + ) + in + if record_name = "EventRecord" then + ("snapshot", "RecordInterface", false) :: fields + else + fields + + let of_ty = function + | SecretString | String -> + Simple {func_suffix= "String"; value_ty= "string"} + | Int -> + Int {func_suffix= "Int"; value_ty= "int"} + | Float -> + Float {func_suffix= "Float"; value_ty= "float64"} + | Bool -> + Simple {func_suffix= "Bool"; value_ty= "bool"} + | DateTime -> + Time {func_suffix= "Time"; value_ty= "time.Time"} + | Enum (name, kv) as ty -> + let name = snake_to_camel name in + let items = + List.map (fun (k, _) -> {value= k; name= name ^ snake_to_camel k}) kv + in + Enum {func_suffix= Json.suffix_of_type ty; value_ty= name; items} + | Set ty as set -> + let fp_ty = Json.suffix_of_type ty in + let ty, _ = Json.string_of_ty_with_enums ty in + Set + { + func_suffix= Json.suffix_of_type set + ; value_ty= ty + ; item_fp_type= fp_ty + } + | Map (ty1, ty2) as ty -> + let name, _ = Json.string_of_ty_with_enums ty in + Map + { + func_suffix= Json.suffix_of_type ty + ; value_ty= name + ; key_ty= Json.suffix_of_type ty1 + ; val_ty= Json.suffix_of_type ty2 + } + | Ref _ as ty -> + let name = Json.suffix_of_type ty in + Ref {func_suffix= name; value_ty= name} + | Record r -> + let name = snake_to_camel r ^ "Record" in + let fields = + List.map + (fun (name, func_suffix, is_option_type) -> + let camel_name = snake_to_camel name in + { + name + ; name_internal= String.uncapitalize_ascii camel_name + ; name_exported= camel_name + ; func_suffix + ; type_option= is_option_type + } + ) + (fields name) + in + Record {func_suffix= name; value_ty= name; fields} + | Option ty -> + Option {func_suffix= Json.suffix_of_type ty} + + let of_serialize params = + `O [("serialize", `A [to_json params]); ("deserialize", `Null)] + + let of_deserialize params = + `O [("serialize", `Null); ("deserialize", `A [to_json params])] + + let event_batch : Mustache.Json.t = + `O + [ + ( "deserialize" + , `A + [ + `O + [ + ("func_name_suffix", `String "EventBatch") + ; ("type", `String "EventBatch") + ; ( "elements" + , `A + [ + `O + [ + ("name", `String "token") + ; ("name_internal", `String "token") + ; ("name_exported", `String "Token") + ; ("func_name_suffix", `String "String") + ] + ; `O + [ + ("name", `String "validRefCounts") + ; ("name_internal", `String "validRefCounts") + ; ("name_exported", `String "ValidRefCounts") + ; ("func_name_suffix", `String "StringToIntMap") + ] + ; `O + [ + ("name", `String "events") + ; ("name_internal", `String "events") + ; ("name_exported", `String "Events") + ; ("func_name_suffix", `String "EventRecordSet") + ] + ] + ) + ] + ] + ) + ] + + let interface : Mustache.Json.t = + `O + [ + ( "deserialize" + , `A + [ + `O + [ + ("func_name_suffix", `String "RecordInterface") + ; ("type", `String "RecordInterface") + ] + ] + ) + ] +end diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index d5de1050a05..4d8a3691f2e 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -43,3 +43,72 @@ module Json : sig val api_errors : Mustache.Json.value list end + +module Convert : sig + type params = {func_suffix: string; value_ty: string} + + type params_of_option = {func_suffix: string} + + type params_of_set = { + func_suffix: string + ; value_ty: string + ; item_fp_type: string + } + + type params_of_record_field = { + name: string + ; name_internal: string + ; name_exported: string + ; func_suffix: string + ; type_option: bool + } + + type params_of_record = { + func_suffix: string + ; value_ty: string + ; fields: params_of_record_field list + } + + type params_of_enum_item = {value: string; name: string} + + type params_of_enum = { + func_suffix: string + ; value_ty: string + ; items: params_of_enum_item list + } + + type params_of_map = { + func_suffix: string + ; value_ty: string + ; key_ty: string + ; val_ty: string + } + + type convert_params = + | Simple of params + | Int of params + | Float of params + | Time of params + | Ref of params + | Option of params_of_option + | Set of params_of_set + | Enum of params_of_enum + | Record of params_of_record + | Map of params_of_map + + val template_of_convert : convert_params -> string + + val to_json : convert_params -> Mustache.Json.value + + val fields : string -> (string * string * bool) list + + val of_ty : Datamodel_types.ty -> convert_params + + val of_serialize : convert_params -> Mustache.Json.t + + val of_deserialize : convert_params -> Mustache.Json.t + + val event_batch : Mustache.Json.t + + val interface : Mustache.Json.t +end From 29a64d26a6030092d9d053d956fcc12ce400bf32 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 20:10:07 +0800 Subject: [PATCH 42/99] CP-47358: Add unit tests for generating convert functions Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_data/batch_convert.go | 29 ++ ocaml/sdk-gen/go/test_data/enum_convert.go | 20 + ocaml/sdk-gen/go/test_data/float_convert.go | 33 ++ ocaml/sdk-gen/go/test_data/int_convert.go | 20 + .../sdk-gen/go/test_data/interface_convert.go | 5 + ocaml/sdk-gen/go/test_data/map_convert.go | 38 ++ ocaml/sdk-gen/go/test_data/option_convert.go | 22 + ocaml/sdk-gen/go/test_data/record_convert.go | 35 ++ ocaml/sdk-gen/go/test_data/ref_convert.go | 13 + ocaml/sdk-gen/go/test_data/set_convert.go | 30 ++ .../go/test_data/simple_type_convert.go | 31 ++ ocaml/sdk-gen/go/test_data/time_convert.go | 29 ++ ocaml/sdk-gen/go/test_gen_go.ml | 375 ++++++++++++++++++ 13 files changed, 680 insertions(+) create mode 100644 ocaml/sdk-gen/go/test_data/batch_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/enum_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/float_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/int_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/interface_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/map_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/option_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/record_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/ref_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/set_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/simple_type_convert.go create mode 100644 ocaml/sdk-gen/go/test_data/time_convert.go diff --git a/ocaml/sdk-gen/go/test_data/batch_convert.go b/ocaml/sdk-gen/go/test_data/batch_convert.go new file mode 100644 index 00000000000..fd0e70607f6 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/batch_convert.go @@ -0,0 +1,29 @@ +func deserializeEventBatch(context string, input interface{}) (batch EventBatch, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + tokenValue, ok := rpcStruct["token"] + if ok && tokenValue != nil { + batch.Token, err = deserializeString(fmt.Sprintf("%s.%s", context, "token"), tokenValue) + if err != nil { + return + } + } + validRefCountsValue, ok := rpcStruct["validRefCounts"] + if ok && validRefCountsValue != nil { + batch.ValidRefCounts, err = deserializeStringToIntMap(fmt.Sprintf("%s.%s", context, "validRefCounts"), validRefCountsValue) + if err != nil { + return + } + } + eventsValue, ok := rpcStruct["events"] + if ok && eventsValue != nil { + batch.Events, err = deserializeEventRecordSet(fmt.Sprintf("%s.%s", context, "events"), eventsValue) + if err != nil { + return + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/enum_convert.go b/ocaml/sdk-gen/go/test_data/enum_convert.go new file mode 100644 index 00000000000..40129c0e5ca --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/enum_convert.go @@ -0,0 +1,20 @@ +func serializeEnumTaskStatusType(context string, value TaskStatusType) (string, error) { + _ = context + return string(value), nil +} + +func deserializeEnumTaskStatusType(context string, input interface{}) (value TaskStatusType, err error) { + strValue, err := deserializeString(context, input) + if err != nil { + return + } + switch strValue { + case "pending": + value = TaskStatusTypePending + case "success": + value = TaskStatusTypeSuccess + default: + err = fmt.Errorf("unable to parse XenAPI response: got value %q for enum %s at %s, but this is not any of the known values", strValue, "TaskStatusType", context) + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/float_convert.go b/ocaml/sdk-gen/go/test_data/float_convert.go new file mode 100644 index 00000000000..736ad6f8111 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/float_convert.go @@ -0,0 +1,33 @@ +//nolint:unparam +func serializeFloat(context string, value float64) (interface{}, error) { + _ = context + if math.IsInf(value, 0) { + if math.IsInf(value, 1) { + return "+Inf", nil + } + return "-Inf", nil + } else if math.IsNaN(value) { + return "NaN", nil + } + return value, nil +} + +func deserializeFloat(context string, input interface{}) (value float64, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.ParseFloat(strValue, 64) + if err != nil { + switch strValue { + case "+Inf": + return math.Inf(1), nil + case "-Inf": + return math.Inf(-1), nil + case "NaN": + return math.NaN(), nil + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/int_convert.go b/ocaml/sdk-gen/go/test_data/int_convert.go new file mode 100644 index 00000000000..0688dffa600 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/int_convert.go @@ -0,0 +1,20 @@ +func serializeInt(context string, value int) (int, error) { + _ = context + return value, nil +} + +func deserializeInt(context string, input interface{}) (value int, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + value, err = strconv.Atoi(strValue) + if err != nil { + floatValue, err1 := strconv.ParseFloat(strValue, 64) + if err1 == nil { + return int(floatValue), nil + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/interface_convert.go b/ocaml/sdk-gen/go/test_data/interface_convert.go new file mode 100644 index 00000000000..fec2c91d133 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/interface_convert.go @@ -0,0 +1,5 @@ +func deserializeRecordInterface(context string, input interface{}) (inter RecordInterface, err error) { + _ = context + inter = input + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/map_convert.go b/ocaml/sdk-gen/go/test_data/map_convert.go new file mode 100644 index 00000000000..fff680ebc35 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/map_convert.go @@ -0,0 +1,38 @@ +func serializeVIFRefToStringMap(context string, goMap map[VIFRef]string) (xenMap map[string]interface{}, err error) { + xenMap = make(map[string]interface{}) + for goKey, goValue := range goMap { + keyContext := fmt.Sprintf("%s[%s]", context, goKey) + xenKey, err := serializeVIFRef(keyContext, goKey) + if err != nil { + return xenMap, err + } + xenValue, err := serializeString(keyContext, goValue) + if err != nil { + return xenMap, err + } + xenMap[xenKey] = xenValue + } + return +} + +func deserializePBDRefToPBDRecordMap(context string, input interface{}) (goMap map[PBDRef]PBDRecord, err error) { + xenMap, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + goMap = make(map[PBDRef]PBDRecord, len(xenMap)) + for xenKey, xenValue := range xenMap { + keyContext := fmt.Sprintf("%s[%s]", context, xenKey) + goKey, err := deserializePBDRef(keyContext, xenKey) + if err != nil { + return goMap, err + } + goValue, err := deserializePBDRecord(keyContext, xenValue) + if err != nil { + return goMap, err + } + goMap[goKey] = goValue + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/option_convert.go b/ocaml/sdk-gen/go/test_data/option_convert.go new file mode 100644 index 00000000000..4a5ce03a70b --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/option_convert.go @@ -0,0 +1,22 @@ +func serializeOptionSrStatRecord(context string, input OptionSrStatRecord) (option interface{}, err error) { + if input == nil { + return + } + option, err = serializeSrStatRecord(context, *input) + if err != nil { + return + } + return +} + +func deserializeOptionSrStatRecord(context string, input interface{}) (option OptionSrStatRecord, err error) { + if input == nil { + return + } + value, err := deserializeSrStatRecord(context, input) + if err != nil { + return + } + option = &value + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/record_convert.go b/ocaml/sdk-gen/go/test_data/record_convert.go new file mode 100644 index 00000000000..55a8e9c3c90 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/record_convert.go @@ -0,0 +1,35 @@ +func serializeVBDRecord(context string, record VBDRecord) (rpcStruct map[string]interface{}, err error) { + rpcStruct = map[string]interface{}{} + rpcStruct["uuid"], err = serializeString(fmt.Sprintf("%s.%s", context, "uuid"), record.UUID) + if err != nil { + return + } + rpcStruct["allowed_operations"], err = serializeEnumVbdOperationsSet(fmt.Sprintf("%s.%s", context, "allowed_operations"), record.AllowedOperations) + if err != nil { + return + } + return +} + +func deserializeVBDRecord(context string, input interface{}) (record VBDRecord, err error) { + rpcStruct, ok := input.(map[string]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "map[string]interface{}", context, reflect.TypeOf(input), input) + return + } + uuidValue, ok := rpcStruct["uuid"] + if ok && uuidValue != nil { + record.UUID, err = deserializeString(fmt.Sprintf("%s.%s", context, "uuid"), uuidValue) + if err != nil { + return + } + } + allowedOperationsValue, ok := rpcStruct["allowed_operations"] + if ok && allowedOperationsValue != nil { + record.AllowedOperations, err = deserializeEnumVbdOperationsSet(fmt.Sprintf("%s.%s", context, "allowed_operations"), allowedOperationsValue) + if err != nil { + return + } + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/ref_convert.go b/ocaml/sdk-gen/go/test_data/ref_convert.go new file mode 100644 index 00000000000..dc23fc88ffa --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/ref_convert.go @@ -0,0 +1,13 @@ +func serializeVMRef(context string, ref VMRef) (string, error) { + _ = context + return string(ref), nil +} + +func deserializeVMRef(context string, input interface{}) (VMRef, error) { + var ref VMRef + value, ok := input.(string) + if !ok { + return ref, fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return VMRef(value), nil +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/set_convert.go b/ocaml/sdk-gen/go/test_data/set_convert.go new file mode 100644 index 00000000000..1d8adb73764 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/set_convert.go @@ -0,0 +1,30 @@ +func serializeSRRefSet(context string, slice []SRRef) (set []interface{}, err error) { + set = make([]interface{}, len(slice)) + for index, item := range slice { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := serializeSRRef(itemContext, item) + if err != nil { + return set, err + } + set[index] = itemValue + } + return +} + +func deserializeStringSet(context string, input interface{}) (slice []string, err error) { + set, ok := input.([]interface{}) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "[]interface{}", context, reflect.TypeOf(input), input) + return + } + slice = make([]string, len(set)) + for index, item := range set { + itemContext := fmt.Sprintf("%s[%d]", context, index) + itemValue, err := deserializeString(itemContext, item) + if err != nil { + return slice, err + } + slice[index] = itemValue + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/simple_type_convert.go b/ocaml/sdk-gen/go/test_data/simple_type_convert.go new file mode 100644 index 00000000000..5b482e0a5b7 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/simple_type_convert.go @@ -0,0 +1,31 @@ +func serializeString(context string, value string) (string, error) { + _ = context + return value, nil +} + +func serializeBool(context string, value bool) (bool, error) { + _ = context + return value, nil +} + +func deserializeString(context string, input interface{}) (value string, err error) { + if input == nil { + return + } + value, ok := input.(string) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "string", context, reflect.TypeOf(input), input) + } + return +} + +func deserializeBool(context string, input interface{}) (value bool, err error) { + if input == nil { + return + } + value, ok := input.(bool) + if !ok { + err = fmt.Errorf("failed to parse XenAPI response: expected Go type %s at %s but got Go type %s with value %v", "bool", context, reflect.TypeOf(input), input) + } + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_data/time_convert.go b/ocaml/sdk-gen/go/test_data/time_convert.go new file mode 100644 index 00000000000..d6da10f4d42 --- /dev/null +++ b/ocaml/sdk-gen/go/test_data/time_convert.go @@ -0,0 +1,29 @@ +var timeFormats = []string{time.RFC3339, "20060102T15:04:05Z", "20060102T15:04:05"} + +//nolint:unparam +func serializeTime(context string, value time.Time) (string, error) { + _ = context + return value.Format(time.RFC3339), nil +} + +func deserializeTime(context string, input interface{}) (value time.Time, err error) { + _ = context + if input == nil { + return + } + strValue := fmt.Sprintf("%v", input) + floatValue, err := strconv.ParseFloat(strValue, 64) + if err != nil { + for _, timeFormat := range timeFormats { + value, err = time.Parse(timeFormat, strValue) + if err == nil { + return value, nil + } + } + return + } + unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) + value = time.Unix(unixTimestamp, 0) + + return +} \ No newline at end of file diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index ef484cf0622..44b9db8b57a 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -344,6 +344,12 @@ let verify_release_member = function | _ -> false +let verify_simple_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | _ -> + false + let release_keys = [ "branding" @@ -379,6 +385,126 @@ let verify_version = function | _ -> false +let verify_simple_convert_keys = ["func_name_suffix"; "type"] + +let verify_simple_convert = function + | `O items -> + schema_check verify_simple_convert_keys verify_simple_convert_member items + | _ -> + false + +let verify_option_convert_member = function + | "func_name_suffix", `String _ -> + true + | _ -> + false + +let option_convert_keys = ["func_name_suffix"] + +let verify_option_convert = function + | `O items -> + schema_check option_convert_keys verify_option_convert_member items + | _ -> + false + +let verify_set_convert_member = function + | "func_name_suffix", `String _ + | "type", `String _ + | "item_func_suffix", `String _ -> + true + | _ -> + false + +let convert_set_keys = ["func_name_suffix"; "type"; "item_func_suffix"] + +let verify_set_convert = function + | `O items -> + schema_check convert_set_keys verify_set_convert_member items + | _ -> + false + +let record_field_keys = + ["name"; "name_internal"; "name_exported"; "func_name_suffix"; "type_option"] + +let verify_record_field_member = function + | "name", `String _ + | "name_internal", `String _ + | "func_name_suffix", `String _ + | "type_option", `Bool _ + | "name_exported", `String _ -> + true + | _ -> + false + +let verify_record_field = function + | `O items -> + schema_check record_field_keys verify_record_field_member items + | _ -> + false + +let verify_record_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | "fields", `A fields -> + List.for_all verify_record_field fields + | _ -> + false + +let convert_record_keys = ["func_name_suffix"; "type"; "fields"] + +let verify_record_convert = function + | `O items -> + schema_check convert_record_keys verify_record_convert_member items + | _ -> + false + +let enum_item_keys = ["value"; "name"] + +let verify_enum_item_member = function + | "name", `String _ | "value", `String _ -> + true + | _ -> + false + +let verify_enum_item = function + | `O members -> + schema_check enum_item_keys verify_enum_item_member members + | _ -> + false + +let enum_convert_keys = ["func_name_suffix"; "type"; "items"] + +let verify_enum_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | "items", `A items -> + List.for_all verify_enum_item items + | _ -> + false + +let verify_enum_convert = function + | `O items -> + schema_check enum_convert_keys verify_enum_convert_member items + | _ -> + false + +let map_convert_keys = ["func_name_suffix"; "type"; "key_type"; "value_type"] + +let verify_map_convert_member = function + | "type", `String _ + | "key_type", `String _ + | "func_name_suffix", `String _ + | "value_type", `String _ -> + true + | _ -> + false + +let verify_map_convert = function + | `O items -> + schema_check map_convert_keys verify_map_convert_member items + | _ -> + false + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -744,6 +870,151 @@ let messages : Mustache.Json.t = ) ] +let simple_type_convert : Mustache.Json.t = + let array = + [ + `O [("func_name_suffix", `String "String"); ("type", `String "string")] + ; `O [("func_name_suffix", `String "Bool"); ("type", `String "bool")] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let int_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Int"); ("type", `String "int")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let float_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Float"); ("type", `String "float64")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let time_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Time"); ("type", `String "time.Time")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let ref_string_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "VMRef"); ("type", `String "VMRef")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let set_convert : Mustache.Json.t = + let serialize = + [ + `O + [ + ("func_name_suffix", `String "SRRefSet") + ; ("type", `String "SRRef") + ; ("item_func_suffix", `String "SRRef") + ] + ] + in + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "StringSet") + ; ("type", `String "string") + ; ("item_func_suffix", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let record_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "VBDRecord") + ; ("type", `String "VBDRecord") + ; ( "fields" + , `A + [ + `O + [ + ("name", `String "uuid") + ; ("name_internal", `String "uuid") + ; ("name_exported", `String "UUID") + ; ("func_name_suffix", `String "String") + ; ("type_option", `Bool false) + ] + ; `O + [ + ("name", `String "allowed_operations") + ; ("name_internal", `String "allowedOperations") + ; ("name_exported", `String "AllowedOperations") + ; ("func_name_suffix", `String "EnumVbdOperationsSet") + ; ("type_option", `Bool false) + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let map_convert : Mustache.Json.t = + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "PBDRefToPBDRecordMap") + ; ("type", `String "map[PBDRef]PBDRecord") + ; ("key_type", `String "PBDRef") + ; ("value_type", `String "PBDRecord") + ] + ] + in + let serialize = + [ + `O + [ + ("func_name_suffix", `String "VIFRefToStringMap") + ; ("type", `String "map[VIFRef]string") + ; ("key_type", `String "VIFRef") + ; ("value_type", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let enum_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "EnumTaskStatusType") + ; ("type", `String "TaskStatusType") + ; ( "items" + , `A + [ + `O + [ + ("name", `String "TaskStatusTypePending") + ; ("value", `String "pending") + ] + ; `O + [ + ("name", `String "TaskStatusTypeSuccess") + ; ("value", `String "success") + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let option_convert : Mustache.Json.t = + let array = [`O [("func_name_suffix", `String "SrStatRecord")]] in + `O [("serialize", `A array); ("deserialize", `A array)] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -777,6 +1048,30 @@ module TemplatesTest = Generic.MakeStateless (struct let option_rendered = "type OptionString *string" + let simple_type_rendered = string_of_file "simple_type_convert.go" + + let int_convert_rendered = string_of_file "int_convert.go" + + let float_convert_rendered = string_of_file "float_convert.go" + + let time_convert_rendered = string_of_file "time_convert.go" + + let string_ref_rendered = string_of_file "ref_convert.go" + + let set_convert_rendered = string_of_file "set_convert.go" + + let record_convert_rendered = string_of_file "record_convert.go" + + let interface_convert_rendered = string_of_file "interface_convert.go" + + let map_convert_rendered = string_of_file "map_convert.go" + + let enum_convert_rendered = string_of_file "enum_convert.go" + + let batch_convert_rendered = string_of_file "batch_convert.go" + + let option_convert_rendered = string_of_file "option_convert.go" + let tests = `QuickAndAutoDocumented [ @@ -789,6 +1084,22 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("APIMessages.mustache", api_messages), api_messages_rendered) ; (("APIVersions.mustache", api_versions), api_versions_rendered) ; (("Option.mustache", option), option_rendered) + ; ( ("ConvertSimpleType.mustache", simple_type_convert) + , simple_type_rendered + ) + ; (("ConvertInt.mustache", int_convert), int_convert_rendered) + ; (("ConvertFloat.mustache", float_convert), float_convert_rendered) + ; (("ConvertTime.mustache", time_convert), time_convert_rendered) + ; (("ConvertRef.mustache", ref_string_convert), string_ref_rendered) + ; (("ConvertSet.mustache", set_convert), set_convert_rendered) + ; (("ConvertRecord.mustache", record_convert), record_convert_rendered) + ; ( ("ConvertInterface.mustache", Convert.interface) + , interface_convert_rendered + ) + ; (("ConvertMap.mustache", map_convert), map_convert_rendered) + ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) + ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) + ; (("ConvertOption.mustache", option_convert), option_convert_rendered) ] end) @@ -1161,6 +1472,69 @@ module GroupParamsTest = Generic.MakeStateless (struct ] end) +module TestConvertGeneratedJson = struct + open Convert + + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) + + let param_types = TypesOfMessages.of_params objects + + let result_types = TypesOfMessages.of_results objects + + let verify_func = function + | Simple _ | Int _ | Float _ | Time _ | Ref _ -> + verify_simple_convert + | Option _ -> + verify_option_convert + | Set _ -> + verify_set_convert + | Record _ -> + verify_record_convert + | Enum _ -> + verify_enum_convert + | Map _ -> + verify_map_convert + + let convert_param_name = function + | Simple _ -> + "simple" + | Int _ -> + "int" + | Float _ -> + "float" + | Time _ -> + "time" + | Ref _ -> + "ref" + | Option _ -> + "option" + | Set _ -> + "set" + | Record _ -> + "record" + | Enum _ -> + "enum" + | Map _ -> + "map" + + let test types () = + List.iter + (fun ty -> + let param = Convert.of_ty ty in + let obj = Convert.to_json param in + let verify_func = verify_func param in + verify (convert_param_name param) verify_func obj + ) + types + + let tests = + [ + ("serialize", `Quick, test param_types) + ; ("deserialize", `Quick, test result_types) + ] +end + let tests = make_suite "gen_go_binding_" [ @@ -1170,6 +1544,7 @@ let tests = ; ("templates", TemplatesTest.tests) ; ("generated_mustache_jsons", TestGeneratedJson.tests) ; ("group_params", GroupParamsTest.tests) + ; ("generated_convert_jsons", TestConvertGeneratedJson.tests) ] let () = Alcotest.run "Gen go binding" tests From 041fc68fa64053484aa3b559c7eaf9df2dffb342 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 24 Apr 2024 09:56:36 +0800 Subject: [PATCH 43/99] CP-47354: Generate messages functions Golang code for all classes Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 292 ++++++++++++++++---------------- 1 file changed, 145 insertions(+), 147 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 44b9db8b57a..0b4bd5a8dea 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -698,6 +698,151 @@ let option = ) ] +let simple_type_convert : Mustache.Json.t = + let array = + [ + `O [("func_name_suffix", `String "String"); ("type", `String "string")] + ; `O [("func_name_suffix", `String "Bool"); ("type", `String "bool")] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let int_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Int"); ("type", `String "int")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let float_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Float"); ("type", `String "float64")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let time_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "Time"); ("type", `String "time.Time")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let ref_string_convert : Mustache.Json.t = + let array = + [`O [("func_name_suffix", `String "VMRef"); ("type", `String "VMRef")]] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let set_convert : Mustache.Json.t = + let serialize = + [ + `O + [ + ("func_name_suffix", `String "SRRefSet") + ; ("type", `String "SRRef") + ; ("item_func_suffix", `String "SRRef") + ] + ] + in + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "StringSet") + ; ("type", `String "string") + ; ("item_func_suffix", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let record_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "VBDRecord") + ; ("type", `String "VBDRecord") + ; ( "fields" + , `A + [ + `O + [ + ("name", `String "uuid") + ; ("name_internal", `String "uuid") + ; ("name_exported", `String "UUID") + ; ("func_name_suffix", `String "String") + ; ("type_option", `Bool false) + ] + ; `O + [ + ("name", `String "allowed_operations") + ; ("name_internal", `String "allowedOperations") + ; ("name_exported", `String "AllowedOperations") + ; ("func_name_suffix", `String "EnumVbdOperationsSet") + ; ("type_option", `Bool false) + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let map_convert : Mustache.Json.t = + let deserialize = + [ + `O + [ + ("func_name_suffix", `String "PBDRefToPBDRecordMap") + ; ("type", `String "map[PBDRef]PBDRecord") + ; ("key_type", `String "PBDRef") + ; ("value_type", `String "PBDRecord") + ] + ] + in + let serialize = + [ + `O + [ + ("func_name_suffix", `String "VIFRefToStringMap") + ; ("type", `String "map[VIFRef]string") + ; ("key_type", `String "VIFRef") + ; ("value_type", `String "String") + ] + ] + in + `O [("serialize", `A serialize); ("deserialize", `A deserialize)] + +let enum_convert : Mustache.Json.t = + let array = + [ + `O + [ + ("func_name_suffix", `String "EnumTaskStatusType") + ; ("type", `String "TaskStatusType") + ; ( "items" + , `A + [ + `O + [ + ("name", `String "TaskStatusTypePending") + ; ("value", `String "pending") + ] + ; `O + [ + ("name", `String "TaskStatusTypeSuccess") + ; ("value", `String "success") + ] + ] + ) + ] + ] + in + `O [("serialize", `A array); ("deserialize", `A array)] + +let option_convert : Mustache.Json.t = + let array = [`O [("func_name_suffix", `String "SrStatRecord")]] in + `O [("serialize", `A array); ("deserialize", `A array)] + let session_messages : Mustache.Json.t = `O [ @@ -842,8 +987,6 @@ let messages : Mustache.Json.t = ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") ; ("func_name_suffix", `String "SessionRef") - ; ("session", `Bool true) - ; ("session_class", `Bool false) ; ("first", `Bool true) ] ; `O @@ -870,151 +1013,6 @@ let messages : Mustache.Json.t = ) ] -let simple_type_convert : Mustache.Json.t = - let array = - [ - `O [("func_name_suffix", `String "String"); ("type", `String "string")] - ; `O [("func_name_suffix", `String "Bool"); ("type", `String "bool")] - ] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let int_convert : Mustache.Json.t = - let array = - [`O [("func_name_suffix", `String "Int"); ("type", `String "int")]] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let float_convert : Mustache.Json.t = - let array = - [`O [("func_name_suffix", `String "Float"); ("type", `String "float64")]] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let time_convert : Mustache.Json.t = - let array = - [`O [("func_name_suffix", `String "Time"); ("type", `String "time.Time")]] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let ref_string_convert : Mustache.Json.t = - let array = - [`O [("func_name_suffix", `String "VMRef"); ("type", `String "VMRef")]] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let set_convert : Mustache.Json.t = - let serialize = - [ - `O - [ - ("func_name_suffix", `String "SRRefSet") - ; ("type", `String "SRRef") - ; ("item_func_suffix", `String "SRRef") - ] - ] - in - let deserialize = - [ - `O - [ - ("func_name_suffix", `String "StringSet") - ; ("type", `String "string") - ; ("item_func_suffix", `String "String") - ] - ] - in - `O [("serialize", `A serialize); ("deserialize", `A deserialize)] - -let record_convert : Mustache.Json.t = - let array = - [ - `O - [ - ("func_name_suffix", `String "VBDRecord") - ; ("type", `String "VBDRecord") - ; ( "fields" - , `A - [ - `O - [ - ("name", `String "uuid") - ; ("name_internal", `String "uuid") - ; ("name_exported", `String "UUID") - ; ("func_name_suffix", `String "String") - ; ("type_option", `Bool false) - ] - ; `O - [ - ("name", `String "allowed_operations") - ; ("name_internal", `String "allowedOperations") - ; ("name_exported", `String "AllowedOperations") - ; ("func_name_suffix", `String "EnumVbdOperationsSet") - ; ("type_option", `Bool false) - ] - ] - ) - ] - ] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let map_convert : Mustache.Json.t = - let deserialize = - [ - `O - [ - ("func_name_suffix", `String "PBDRefToPBDRecordMap") - ; ("type", `String "map[PBDRef]PBDRecord") - ; ("key_type", `String "PBDRef") - ; ("value_type", `String "PBDRecord") - ] - ] - in - let serialize = - [ - `O - [ - ("func_name_suffix", `String "VIFRefToStringMap") - ; ("type", `String "map[VIFRef]string") - ; ("key_type", `String "VIFRef") - ; ("value_type", `String "String") - ] - ] - in - `O [("serialize", `A serialize); ("deserialize", `A deserialize)] - -let enum_convert : Mustache.Json.t = - let array = - [ - `O - [ - ("func_name_suffix", `String "EnumTaskStatusType") - ; ("type", `String "TaskStatusType") - ; ( "items" - , `A - [ - `O - [ - ("name", `String "TaskStatusTypePending") - ; ("value", `String "pending") - ] - ; `O - [ - ("name", `String "TaskStatusTypeSuccess") - ; ("value", `String "success") - ] - ] - ) - ] - ] - in - `O [("serialize", `A array); ("deserialize", `A array)] - -let option_convert : Mustache.Json.t = - let array = [`O [("func_name_suffix", `String "SrStatRecord")]] in - `O [("serialize", `A array); ("deserialize", `A array)] - module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t From 2e871a189507952e1ecc232df90987dd1338f8a9 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Mon, 6 May 2024 20:15:36 +0800 Subject: [PATCH 44/99] CP-47354: add unit tests for `func_name_suffix` and `string_of_ty_with_enums` Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/test_gen_go.ml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 0b4bd5a8dea..34d20b79621 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -987,6 +987,8 @@ let messages : Mustache.Json.t = ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") ; ("func_name_suffix", `String "SessionRef") + ; ("session", `Bool true) + ; ("session_class", `Bool false) ; ("first", `Bool true) ] ; `O From f2c02d8ed461dced0c4b35c9fafd42bb024864c2 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:15:34 +0800 Subject: [PATCH 45/99] CP-48855: render options Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 12 ++++ ocaml/sdk-gen/go/test_gen_go.ml | 107 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 0f9760dbdab..e6608a21c43 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -448,6 +448,18 @@ module Json = struct |> List.filter_map (function Option ty -> Some ty | _ -> None) |> List.map of_option + let of_option ty = + let name, _ = string_of_ty_with_enums ty in + `O + [ + ("type", `String name); ("type_name_suffix", `String (suffix_of_type ty)) + ] + + let of_options types = + types + |> List.filter_map (function Option ty -> Some ty | _ -> None) + |> List.map of_option + let xenapi objs = List.map (fun obj -> diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 34d20b79621..7b44285deb7 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -1015,6 +1015,21 @@ let messages : Mustache.Json.t = ) ] +let option = + `O + [ + ( "option" + , `A + [ + `O + [ + ("type", `String "string") + ; ("type_name_suffix", `String "String") + ] + ] + ) + ] + module TemplatesTest = Generic.MakeStateless (struct module Io = struct type input_t = string * Mustache.Json.t @@ -1072,6 +1087,8 @@ module TemplatesTest = Generic.MakeStateless (struct let option_convert_rendered = string_of_file "option_convert.go" + let option_rendered = "type OptionString *string" + let tests = `QuickAndAutoDocumented [ @@ -1100,6 +1117,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) ; (("ConvertOption.mustache", option_convert), option_convert_rendered) + ; (("Option.mustache", option), option_rendered) ] end) @@ -1535,6 +1553,95 @@ module TestConvertGeneratedJson = struct ] end +module StringOfTyWithEnumsTest = struct + open Datamodel_types + module StringMap = Json.StringMap + + let verify description verify_func actual = + Alcotest.(check bool) description true (verify_func actual) + + let verify_string (ty, enums) = ty = "string" && enums = StringMap.empty + + let test_string () = + let ty, enums = Json.string_of_ty_with_enums String in + verify "String" verify_string (ty, enums) + + let test_secret_string () = + let ty, enums = Json.string_of_ty_with_enums SecretString in + verify "SecretString" verify_string (ty, enums) + + let verify_float (ty, enums) = ty = "float64" && enums = StringMap.empty + + let test_float () = + let ty, enums = Json.string_of_ty_with_enums Float in + verify "Float" verify_float (ty, enums) + + let verify_bool (ty, enums) = ty = "bool" && enums = StringMap.empty + + let test_bool () = + let ty, enums = Json.string_of_ty_with_enums Bool in + verify "bool" verify_bool (ty, enums) + + let verify_datetime (ty, enums) = ty = "time.Time" && enums = StringMap.empty + + let test_datetime () = + let ty, enums = Json.string_of_ty_with_enums DateTime in + verify "datetime" verify_datetime (ty, enums) + + let enum_lst = [("a", "b"); ("c", "d")] + + let verify_enum (ty, enums) = + ty = "UpdateSync" && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_enum () = + let ty, enums = + Json.string_of_ty_with_enums (Enum ("update_sync", enum_lst)) + in + verify "enum" verify_enum (ty, enums) + + let verify_ref (ty, enums) = ty = "PoolRef" && enums = StringMap.empty + + let test_ref () = + let ty, enums = Json.string_of_ty_with_enums (Ref "pool") in + verify "ref" verify_ref (ty, enums) + + let verify_record (ty, enums) = ty = "PoolRecord" && enums = StringMap.empty + + let test_record () = + let ty, enums = Json.string_of_ty_with_enums (Record "pool") in + verify "record" verify_record (ty, enums) + + let verify_option (ty, enums) = ty = "OptionString" && enums = StringMap.empty + + let test_option () = + let ty, enums = Json.string_of_ty_with_enums (Option String) in + verify "option" verify_string (ty, enums) + + let verify_map (ty, enums) = + ty = "map[int]UpdateSync" + && enums = StringMap.singleton "UpdateSync" enum_lst + + let test_map () = + let ty, enums = + Json.string_of_ty_with_enums (Map (Int, Enum ("update_sync", enum_lst))) + in + verify "map" verify_map (ty, enums) + + let tests = + [ + ("String", `Quick, test_string) + ; ("SecretString", `Quick, test_secret_string) + ; ("Float", `Quick, test_float) + ; ("Bool", `Quick, test_bool) + ; ("DateTime", `Quick, test_datetime) + ; ("Enum", `Quick, test_enum) + ; ("Ref", `Quick, test_ref) + ; ("Record", `Quick, test_record) + ; ("Option", `Quick, test_option) + ; ("Map", `Quick, test_map) + ] +end + let tests = make_suite "gen_go_binding_" [ From 2e5635f3919e328493ff26d4a02c7f27f5ad6d91 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Tue, 30 Apr 2024 22:26:09 +0800 Subject: [PATCH 46/99] CP-48855: render APIVersion Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 12 -- ocaml/sdk-gen/go/test_gen_go.ml | 245 +++++++++--------------------- 2 files changed, 70 insertions(+), 187 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index e6608a21c43..0f9760dbdab 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -448,18 +448,6 @@ module Json = struct |> List.filter_map (function Option ty -> Some ty | _ -> None) |> List.map of_option - let of_option ty = - let name, _ = string_of_ty_with_enums ty in - `O - [ - ("type", `String name); ("type_name_suffix", `String (suffix_of_type ty)) - ] - - let of_options types = - types - |> List.filter_map (function Option ty -> Some ty | _ -> None) - |> List.map of_option - let xenapi objs = List.map (fun obj -> diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 7b44285deb7..02ee17cb2b7 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -332,6 +332,12 @@ let verify_msgs_or_errors lst = in List.for_all verify_msg_or_error lst +let verify_simple_convert_member = function + | "func_name_suffix", `String _ | "type", `String _ -> + true + | _ -> + false + let verify_release_member = function | "branding", `String _ | "code_name", `String _ -> true @@ -344,47 +350,6 @@ let verify_release_member = function | _ -> false -let verify_simple_convert_member = function - | "func_name_suffix", `String _ | "type", `String _ -> - true - | _ -> - false - -let release_keys = - [ - "branding" - ; "code_name" - ; "version_major" - ; "version_minor" - ; "first" - ; "version_index" - ] - -let verify_release = function - | `O members -> - schema_check release_keys verify_release_member members - | _ -> - false - -let version_keys = - ["API_VERSION_MAJOR"; "API_VERSION_MINOR"; "latest_version_index"; "releases"] - -let verify_version_member = function - | "latest_version_index", `Float _ - | "API_VERSION_MAJOR", `Float _ - | "API_VERSION_MINOR", `Float _ -> - true - | "releases", `A releases -> - List.for_all verify_release releases - | _ -> - false - -let verify_version = function - | `O members -> - schema_check version_keys verify_version_member members - | _ -> - false - let verify_simple_convert_keys = ["func_name_suffix"; "type"] let verify_simple_convert = function @@ -505,6 +470,41 @@ let verify_map_convert = function | _ -> false +let release_keys = + [ + "branding" + ; "code_name" + ; "version_major" + ; "version_minor" + ; "first" + ; "version_index" + ] + +let verify_release = function + | `O members -> + schema_check release_keys verify_release_member members + | _ -> + false + +let version_keys = + ["API_VERSION_MAJOR"; "API_VERSION_MINOR"; "latest_version_index"; "releases"] + +let verify_version_member = function + | "latest_version_index", `Float _ + | "API_VERSION_MAJOR", `Float _ + | "API_VERSION_MINOR", `Float _ -> + true + | "releases", `A releases -> + List.for_all verify_release releases + | _ -> + false + +let verify_version = function + | `O members -> + schema_check version_keys verify_version_member members + | _ -> + false + let rec string_of_json_value (value : Mustache.Json.value) : string = match value with | `Null -> @@ -656,48 +656,6 @@ let api_messages : Mustache.Json.t = ) ] -let api_versions : Mustache.Json.t = - `O - [ - ("latest_version_index", `Float 2.) - ; ( "releases" - , `A - [ - `O - [ - ("branding", `String "XenServer 4.0") - ; ("code_name", `String "rio") - ; ("version_major", `Float 1.) - ; ("version_minor", `Float 1.) - ; ("first", `Bool true) - ] - ; `O - [ - ("branding", `String "XenServer 4.1") - ; ("code_name", `String "miami") - ; ("version_major", `Float 1.) - ; ("version_minor", `Float 2.) - ; ("first", `Bool false) - ] - ] - ) - ] - -let option = - `O - [ - ( "option" - , `A - [ - `O - [ - ("type", `String "string") - ; ("type_name_suffix", `String "String") - ] - ] - ) - ] - let simple_type_convert : Mustache.Json.t = let array = [ @@ -1015,6 +973,33 @@ let messages : Mustache.Json.t = ) ] +let api_versions : Mustache.Json.t = + `O + [ + ("latest_version_index", `Float 2.) + ; ( "releases" + , `A + [ + `O + [ + ("branding", `String "XenServer 4.0") + ; ("code_name", `String "rio") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 1.) + ; ("first", `Bool true) + ] + ; `O + [ + ("branding", `String "XenServer 4.1") + ; ("code_name", `String "miami") + ; ("version_major", `Float 1.) + ; ("version_minor", `Float 2.) + ; ("first", `Bool false) + ] + ] + ) + ] + let option = `O [ @@ -1087,8 +1072,6 @@ module TemplatesTest = Generic.MakeStateless (struct let option_convert_rendered = string_of_file "option_convert.go" - let option_rendered = "type OptionString *string" - let tests = `QuickAndAutoDocumented [ @@ -1117,6 +1100,7 @@ module TemplatesTest = Generic.MakeStateless (struct ; (("ConvertEnum.mustache", enum_convert), enum_convert_rendered) ; (("ConvertBatch.mustache", Convert.event_batch), batch_convert_rendered) ; (("ConvertOption.mustache", option_convert), option_convert_rendered) + ; (("APIVersions.mustache", api_versions), api_versions_rendered) ; (("Option.mustache", option), option_rendered) ] end) @@ -1180,95 +1164,6 @@ module SuffixOfTypeTest = Generic.MakeStateless (struct ] end) -module StringOfTyWithEnumsTest = struct - open Datamodel_types - module StringMap = Json.StringMap - - let verify description verify_func actual = - Alcotest.(check bool) description true (verify_func actual) - - let verify_string (ty, enums) = ty = "string" && enums = StringMap.empty - - let test_string () = - let ty, enums = Json.string_of_ty_with_enums String in - verify "String" verify_string (ty, enums) - - let test_secret_string () = - let ty, enums = Json.string_of_ty_with_enums SecretString in - verify "SecretString" verify_string (ty, enums) - - let verify_float (ty, enums) = ty = "float64" && enums = StringMap.empty - - let test_float () = - let ty, enums = Json.string_of_ty_with_enums Float in - verify "Float" verify_float (ty, enums) - - let verify_bool (ty, enums) = ty = "bool" && enums = StringMap.empty - - let test_bool () = - let ty, enums = Json.string_of_ty_with_enums Bool in - verify "bool" verify_bool (ty, enums) - - let verify_datetime (ty, enums) = ty = "time.Time" && enums = StringMap.empty - - let test_datetime () = - let ty, enums = Json.string_of_ty_with_enums DateTime in - verify "datetime" verify_datetime (ty, enums) - - let enum_lst = [("a", "b"); ("c", "d")] - - let verify_enum (ty, enums) = - ty = "UpdateSync" && enums = StringMap.singleton "UpdateSync" enum_lst - - let test_enum () = - let ty, enums = - Json.string_of_ty_with_enums (Enum ("update_sync", enum_lst)) - in - verify "enum" verify_enum (ty, enums) - - let verify_ref (ty, enums) = ty = "PoolRef" && enums = StringMap.empty - - let test_ref () = - let ty, enums = Json.string_of_ty_with_enums (Ref "pool") in - verify "ref" verify_ref (ty, enums) - - let verify_record (ty, enums) = ty = "PoolRecord" && enums = StringMap.empty - - let test_record () = - let ty, enums = Json.string_of_ty_with_enums (Record "pool") in - verify "record" verify_record (ty, enums) - - let verify_option (ty, enums) = ty = "OptionString" && enums = StringMap.empty - - let test_option () = - let ty, enums = Json.string_of_ty_with_enums (Option String) in - verify "option" verify_option (ty, enums) - - let verify_map (ty, enums) = - ty = "map[int]UpdateSync" - && enums = StringMap.singleton "UpdateSync" enum_lst - - let test_map () = - let ty, enums = - Json.string_of_ty_with_enums (Map (Int, Enum ("update_sync", enum_lst))) - in - verify "map" verify_map (ty, enums) - - let tests = - [ - ("String", `Quick, test_string) - ; ("SecretString", `Quick, test_secret_string) - ; ("Float", `Quick, test_float) - ; ("Bool", `Quick, test_bool) - ; ("DateTime", `Quick, test_datetime) - ; ("Enum", `Quick, test_enum) - ; ("Ref", `Quick, test_ref) - ; ("Record", `Quick, test_record) - ; ("Option", `Quick, test_option) - ; ("Map", `Quick, test_map) - ] -end - module GroupParamsTest = Generic.MakeStateless (struct open Datamodel_types open Datamodel_common @@ -1615,7 +1510,7 @@ module StringOfTyWithEnumsTest = struct let test_option () = let ty, enums = Json.string_of_ty_with_enums (Option String) in - verify "option" verify_string (ty, enums) + verify "option" verify_option (ty, enums) let verify_map (ty, enums) = ty = "map[int]UpdateSync" From 22e7ffae75918fd5a653bd66b48d223ed2211acb Mon Sep 17 00:00:00 2001 From: xueqingz Date: Mon, 6 May 2024 05:47:10 +0000 Subject: [PATCH 47/99] CP-47357: Add a Go JSON-RPC client file Signed-off-by: xueqingz --- .../sdk-gen/go/autogen/src/jsonrpc_client.go | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go new file mode 100644 index 00000000000..c7448479428 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -0,0 +1,225 @@ +package xenapi + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "strings" + "time" +) + +type Request struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` + ID int `json:"id"` +} + +type ResponseError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +type Response struct { + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *ResponseError `json:"error,omitempty"` + ID int `json:"id"` +} + +func paramsParse(params ...interface{}) interface{} { + var finalParams interface{} + finalParams = params + if len(params) == 1 { + if params[0] != nil { + var typeOf reflect.Type + typeOf = reflect.TypeOf(params[0]) + for typeOf != nil && typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + typeArr := []reflect.Kind{reflect.Struct, reflect.Array, reflect.Slice, reflect.Interface, reflect.Map} + if typeOf != nil { + for _, value := range typeArr { + if value == typeOf.Kind() { + finalParams = params[0] + break + } + } + } + } + } + return finalParams +} + +type rpcClient struct { + endpoint string + httpClient *http.Client + headers map[string]string +} + +func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http.Request, error) { + dataByte, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.endpoint, bytes.NewReader(dataByte)) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + request.Header.Set("Content-Type", "application/json; charset=utf-8") + request.Header.Set("Accept", "application/json") + request.Header.Set("User-Agent", "XenAPI/" + APIVersionLatest.String()) + for k, v := range client.headers { + request.Header.Set(k, v) + } + + return request, nil +} + +func convertUnhandledJSONData(jsonBytes []byte) []byte { + jsonString := string(jsonBytes) + jsonString = strings.ReplaceAll(jsonString, ":Infinity", ":\"+Inf\"") + jsonString = strings.ReplaceAll(jsonString, ":-Infinity", ":\"-Inf\"") + jsonString = strings.ReplaceAll(jsonString, ":NaN", ":\"NaN\"") + + return []byte(jsonString) +} + +func (client *rpcClient) call(ctx context.Context, methodName string, params ...interface{}) (*Response, error) { + request := &Request{ + ID: 0, + Method: methodName, + Params: paramsParse(params...), + JSONRPC: "2.0", + } + + httpRequest, err := client.newRequest(ctx, request) + if err != nil { + return nil, fmt.Errorf("could not create request for %v() : %w", request.Method, err) + } + + httpResponse, err := client.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("call %v() on %v. Error making http request: %w", request.Method, httpRequest.URL.Redacted(), err) + } + defer httpResponse.Body.Close() + + var rpcResponse *Response + body, err := io.ReadAll(httpResponse.Body) + if err != nil { + return nil, fmt.Errorf("call %v() on %v status code: %v. Could not read response body: %w", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err) + } + body = convertUnhandledJSONData(body) + err = json.Unmarshal(body, &rpcResponse) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("call %v() on %v status code: %v. Could not decode response body: %w", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err) + } + + if rpcResponse == nil { + return nil, fmt.Errorf("call %v() on %v status code: %v. Response missing", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode) + } + + return rpcResponse, nil +} + +func (client *rpcClient) sendCall(methodName string, params ...interface{}) (result interface{}, err error) { + response, err := client.call(context.Background(), methodName, params...) + if err != nil { + return + } + + if response.Error != nil { + errString := fmt.Sprintf("API error: code %d, message %s", response.Error.Code, response.Error.Message) + if response.Error.Data != nil { + errString += fmt.Sprintf(", data %v", response.Error.Data) + } + err = errors.New(errString) + return + } + + result = response.Result + return +} + +type SecureOpts struct { + ServerCert string + ClientCert string + ClientKey string +} + +type ClientOpts struct { + URL string + SecureOpts *SecureOpts + Timeout int + Headers map[string]string +} + +func newJSONRPCClient(opts *ClientOpts) *rpcClient { + client := &rpcClient{ + endpoint: fmt.Sprintf("%s%s", opts.URL, "/jsonrpc"), + httpClient: &http.Client{}, + headers: make(map[string]string), + } + + if strings.HasPrefix(opts.URL, "https://") { + skipVerify := true + caCertPool := x509.NewCertPool() + certs := []tls.Certificate{} + if opts.SecureOpts != nil { + skipVerify = false + if opts.SecureOpts.ServerCert != "" { + caCert, err := os.ReadFile(opts.SecureOpts.ServerCert) + if err != nil { + log.Fatal(err) + } + caCertPool.AppendCertsFromPEM(caCert) + } + if opts.SecureOpts.ClientCert != "" || opts.SecureOpts.ClientKey != "" { + if opts.SecureOpts.ClientCert == "" { + log.Fatal(errors.New("missing client certificate")) + } + if opts.SecureOpts.ClientKey == "" { + log.Fatal(errors.New("missing client private key")) + } + cert, err := tls.LoadX509KeyPair(opts.SecureOpts.ClientCert, opts.SecureOpts.ClientKey) + if err != nil { + log.Fatal(err) + } + certs = []tls.Certificate{cert} + } + } + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + Certificates: certs, + InsecureSkipVerify: skipVerify, // #nosec + } + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + client.httpClient.Transport = transport + } + + if opts.Timeout != 0 { + client.httpClient.Timeout = time.Duration(opts.Timeout) * time.Second + } + + if opts.Headers != nil { + for k, v := range opts.Headers { + client.headers[k] = v + } + } + + return client +} From 890ae8d47303b6737632772a4b1e2c8e3f70af97 Mon Sep 17 00:00:00 2001 From: xueqingz Date: Tue, 14 May 2024 10:18:40 +0000 Subject: [PATCH 48/99] CP-47357: fix review issues Signed-off-by: xueqingz --- .../sdk-gen/go/autogen/src/jsonrpc_client.go | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go index c7448479428..b4dd2d601e5 100644 --- a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -11,8 +11,8 @@ import ( "io" "log" "net/http" + "net/url" "os" - "reflect" "strings" "time" ) @@ -37,30 +37,6 @@ type Response struct { ID int `json:"id"` } -func paramsParse(params ...interface{}) interface{} { - var finalParams interface{} - finalParams = params - if len(params) == 1 { - if params[0] != nil { - var typeOf reflect.Type - typeOf = reflect.TypeOf(params[0]) - for typeOf != nil && typeOf.Kind() == reflect.Ptr { - typeOf = typeOf.Elem() - } - typeArr := []reflect.Kind{reflect.Struct, reflect.Array, reflect.Slice, reflect.Interface, reflect.Map} - if typeOf != nil { - for _, value := range typeArr { - if value == typeOf.Kind() { - finalParams = params[0] - break - } - } - } - } - } - return finalParams -} - type rpcClient struct { endpoint string httpClient *http.Client @@ -80,7 +56,7 @@ func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http request.Header.Set("Content-Type", "application/json; charset=utf-8") request.Header.Set("Accept", "application/json") - request.Header.Set("User-Agent", "XenAPI/" + APIVersionLatest.String()) + request.Header.Set("User-Agent", "XenAPI/"+APIVersionLatest.String()) for k, v := range client.headers { request.Header.Set(k, v) } @@ -101,7 +77,7 @@ func (client *rpcClient) call(ctx context.Context, methodName string, params ... request := &Request{ ID: 0, Method: methodName, - Params: paramsParse(params...), + Params: params, JSONRPC: "2.0", } @@ -173,25 +149,32 @@ func newJSONRPCClient(opts *ClientOpts) *rpcClient { headers: make(map[string]string), } - if strings.HasPrefix(opts.URL, "https://") { + u, err := url.Parse(opts.URL) + if err != nil { + log.Fatal(err) + } + if strings.Compare(u.Scheme, "https") == 0 { skipVerify := true caCertPool := x509.NewCertPool() certs := []tls.Certificate{} if opts.SecureOpts != nil { - skipVerify = false if opts.SecureOpts.ServerCert != "" { + skipVerify = false caCert, err := os.ReadFile(opts.SecureOpts.ServerCert) if err != nil { log.Fatal(err) } - caCertPool.AppendCertsFromPEM(caCert) + ok := caCertPool.AppendCertsFromPEM(caCert) + if !ok { + log.Fatal("failed to parse CA certificate") + } } if opts.SecureOpts.ClientCert != "" || opts.SecureOpts.ClientKey != "" { if opts.SecureOpts.ClientCert == "" { - log.Fatal(errors.New("missing client certificate")) + log.Fatal("missing client certificate") } if opts.SecureOpts.ClientKey == "" { - log.Fatal(errors.New("missing client private key")) + log.Fatal("missing client private key") } cert, err := tls.LoadX509KeyPair(opts.SecureOpts.ClientCert, opts.SecureOpts.ClientKey) if err != nil { @@ -204,6 +187,12 @@ func newJSONRPCClient(opts *ClientOpts) *rpcClient { RootCAs: caCertPool, Certificates: certs, InsecureSkipVerify: skipVerify, // #nosec + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, } transport := &http.Transport{ TLSClientConfig: tlsConfig, From 9ea67da37fd18b6908c822560fce9d46acc846d1 Mon Sep 17 00:00:00 2001 From: Fei Su Date: Thu, 11 Apr 2024 16:49:01 +0800 Subject: [PATCH 49/99] CP-47367 Add type checking for generated SDK Go files Signed-off-by: Fei Su --- .github/workflows/generate-and-build-sdks.yml | 3 + .github/workflows/go-lint/action.yml | 14 ++ .github/workflows/sdk-ci/action.yml | 6 + .golangci.yml | 142 ++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 .github/workflows/go-lint/action.yml create mode 100644 .github/workflows/sdk-ci/action.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/generate-and-build-sdks.yml b/.github/workflows/generate-and-build-sdks.yml index 80b32b5c8d9..be83ba57f1a 100644 --- a/.github/workflows/generate-and-build-sdks.yml +++ b/.github/workflows/generate-and-build-sdks.yml @@ -24,6 +24,9 @@ jobs: shell: bash run: opam exec -- make sdk + - name: Run CI for SDKs + uses: ./.github/workflows/sdk-ci + - name: Store C# SDK source uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/go-lint/action.yml b/.github/workflows/go-lint/action.yml new file mode 100644 index 00000000000..d041aa4627a --- /dev/null +++ b/.github/workflows/go-lint/action.yml @@ -0,0 +1,14 @@ +name: Go Lint +runs: + using: composite + steps: + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: v1.57.2 + working-directory: ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src + args: --config=${{ github.workspace }}/.golangci.yml diff --git a/.github/workflows/sdk-ci/action.yml b/.github/workflows/sdk-ci/action.yml new file mode 100644 index 00000000000..3113b1b91b4 --- /dev/null +++ b/.github/workflows/sdk-ci/action.yml @@ -0,0 +1,6 @@ +name: Continuous Integration for SDKs +runs: + using: composite + steps: + - name: Lint for Go SDK + uses: ./.github/workflows/go-lint diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000000..6ddf8fa2994 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,142 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + +linters: + disable-all: true + enable: + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - copyloopvar # detects places where loop variables are copied + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocritic # provides diagnostics that check for bugs, performance and style issues + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomnd # detects magic numbers + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - intrange # finds places where for loops could make use of an integer range + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - musttag # enforces field tags in (un)marshaled structs + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - spancheck # checks for mistakes with OpenTelemetry/Census spans + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + - gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + - importas # enforces consistent import aliases + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - testpackage # makes you use a separate _test package + - decorder # checks declaration order and count of types, constants, variables and functions + - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - tagliatelle # checks the struct tags + - godox # detects FIXME, TODO and other comment keywords + - gci # controls golang package import order and makes it always deterministic + - tagalign # checks that struct tags are well aligned + - wrapcheck # checks that errors returned from external packages are wrapped + - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + + + ## disabled + #- goconst # finds repeated strings that could be replaced by a constant + #- gochecknoglobals # checks that no global variables exist + #- gocyclo # computes and checks the cyclomatic complexity of functions + #- nestif # reports deeply nested if statements + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- funlen # tool for detection of long functions + #- godot # checks if comments end in a period + #- stylecheck # is a replacement for golint + #- gocognit # computes and checks the cognitive complexity of functions + #- nakedret # finds naked returns in functions greater than a specified function length + #- lll # reports long lines + #- nonamedreturns # reports all named returns + #- cyclop # checks function and package cyclomatic complexity + #- dupl # tool for code clone detection + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dupword # [useless without config] checks for duplicate words in the source code + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- grouper # analyzes expression groups + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + +output: + # Sort results by the order defined in `sort-order`. + # Default: false + sort-results: true + # Order to use when sorting results. + # Require `sort-results` to `true`. + # Possible values: `file`, `linter`, and `severity`. + # + # If the severity values are inside the following list, they are ordered in this order: + # 1. error + # 2. warning + # 3. high + # 4. medium + # 5. low + # Either they are sorted alphabetically. + # + # Default: ["file"] + sort-order: + - linter + - severity + - file # filepath, line, and column. + # Show statistics per linter. + # Default: false + show-stats: true From a3bff497409cd1fea2c5314e10c235973a610921 Mon Sep 17 00:00:00 2001 From: Luca Zhang Date: Wed, 15 May 2024 15:45:24 +0800 Subject: [PATCH 50/99] CP-49350: fix variable naming in Go SDK Signed-off-by: Luca Zhang --- ocaml/sdk-gen/go/gen_go_helper.ml | 39 +++++++++++++++--------------- ocaml/sdk-gen/go/gen_go_helper.mli | 2 +- ocaml/sdk-gen/go/test_gen_go.ml | 18 +++++++++----- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 0f9760dbdab..02144e89567 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -41,16 +41,22 @@ let acronyms = let is_acronym word = StringSet.mem word acronyms -let snake_to_camel (s : string) : string = - Astring.String.cuts ~sep:"_" s - |> List.concat_map (fun s -> Astring.String.cuts ~sep:"-" s) - |> List.map (function - | s when is_acronym s -> - String.uppercase_ascii s - | s -> - String.capitalize_ascii s - ) - |> String.concat "" +let amend_acronym = function + | s when is_acronym s -> + String.uppercase_ascii s + | s -> + String.capitalize_ascii s + +let snake_to_camel ?(internal = false) (s : string) : string = + let words = + Astring.String.cuts ~sep:"_" s + |> List.concat_map (fun s -> Astring.String.cuts ~sep:"-" s) + in + match (internal, words) with + | true, hd :: tl -> + String.lowercase_ascii hd :: List.map amend_acronym tl |> String.concat "" + | _ -> + words |> List.map amend_acronym |> String.concat "" let records = List.map @@ -310,7 +316,7 @@ module Json = struct let of_param param = let name_internal name = - let name = name |> snake_to_camel |> String.uncapitalize_ascii in + let name = snake_to_camel ~internal:true name in match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name in let suffix_of_type = suffix_of_type param.param_type in @@ -452,7 +458,7 @@ module Json = struct List.map (fun obj -> let obj_name = snake_to_camel obj.name in - let name_internal = String.uncapitalize_ascii obj_name in + let name_internal = snake_to_camel ~internal:true obj.name in let fields = Datamodel_utils.fields_of_obj obj in let types = List.map (fun field -> field.ty) fields |> Listext.List.setify @@ -481,14 +487,7 @@ module Json = struct let of_api_message_or_error info = let xapi_constants_renaming (s : string) : string = String.split_on_char '_' s - |> List.map (fun seg -> - let lower = String.lowercase_ascii seg in - match lower with - | s when is_acronym s -> - String.uppercase_ascii lower - | _ -> - String.capitalize_ascii lower - ) + |> List.map (fun seg -> String.lowercase_ascii seg |> amend_acronym) |> String.concat "" in `O diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 4d8a3691f2e..623af766361 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -13,7 +13,7 @@ val ( // ) : string -> string -> string -val snake_to_camel : string -> string +val snake_to_camel : ?internal:bool -> string -> string val render_template : string -> Mustache.Json.t -> ?newline:bool -> unit -> string diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 56a1fabc72d..8f161730fad 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -22,23 +22,29 @@ let string_of_file filename = module SnakeToCamelTest = Generic.MakeStateless (struct module Io = struct - type input_t = string + type input_t = bool * string type output_t = string - let string_of_input_t = Test_printers.string + let string_of_input_t = Fmt.(str "%a" Dump.(pair bool string)) let string_of_output_t = Test_printers.string end - let transform = snake_to_camel + let transform (internal, str) = snake_to_camel ~internal str let tests = `QuickAndAutoDocumented [ - ("ni_hao-Nanjin", "NiHaoNanjin") - ; ("ni_hao", "NiHao") - ; ("nanjing", "Nanjing") + ((false, "ni_hao-Nanjin"), "NiHaoNanjin") + ; ((false, "ni_hao"), "NiHao") + ; ((false, "nanjing"), "Nanjing") + ; ((false, "uuid"), "UUID") + ; ((false, "get_by_uuid"), "GetByUUID") + ; ((true, "uuid"), "uuid") + ; ((true, "PIF_Metrics"), "pifMetrics") + ; ((true, "VM_guest_metrics"), "vmGuestMetrics") + ; ((true, "Network"), "network") ] end) From 9439b256db6b7d81e936dc85303f961d49224ed6 Mon Sep 17 00:00:00 2001 From: Fei Su Date: Tue, 21 May 2024 16:02:09 +0800 Subject: [PATCH 51/99] Set up Github Action for go SDK component test (#5588) * CP-48777 Set up Github Action for go sdk component test Signed-off-by: Fei Su * update for using the generated sdk Signed-off-by: Fei Su --------- Signed-off-by: Fei Su --- .github/workflows/go-ci/action.yml | 22 ++++++ .github/workflows/go-lint/action.yml | 14 ---- .github/workflows/sdk-ci/action.yml | 22 ++++-- ocaml/sdk-gen/component-test/README.md | 67 +++++++++++++++++ .../component-test/jsonrpc-client/go/go.mod | 5 ++ .../jsonrpc-client/go/main_test.go | 52 ++++++++++++++ .../jsonrpc-server/requirements.txt | 1 + .../component-test/jsonrpc-server/server.py | 72 +++++++++++++++++++ ocaml/sdk-gen/component-test/run-tests.sh | 38 ++++++++++ .../sdk-gen/component-test/spec/session.json | 40 +++++++++++ 10 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/go-ci/action.yml delete mode 100644 .github/workflows/go-lint/action.yml create mode 100644 ocaml/sdk-gen/component-test/README.md create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-server/server.py create mode 100644 ocaml/sdk-gen/component-test/run-tests.sh create mode 100644 ocaml/sdk-gen/component-test/spec/session.json diff --git a/.github/workflows/go-ci/action.yml b/.github/workflows/go-ci/action.yml new file mode 100644 index 00000000000..6dc66224fe0 --- /dev/null +++ b/.github/workflows/go-ci/action.yml @@ -0,0 +1,22 @@ +name: 'Run CI for Go SDK' +runs: + using: 'composite' + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + + - name: Lint for Go SDK + uses: golangci/golangci-lint-action@v4 + with: + version: v1.57.2 + working-directory: ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src + args: --config=${{ github.workspace }}/.golangci.yml + + - name: Run CI for Go SDK + shell: bash + run: | + cd ./ocaml/sdk-gen/component-test/ + cp -r ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src jsonrpc-client/go/goSDK + bash run-tests.sh \ No newline at end of file diff --git a/.github/workflows/go-lint/action.yml b/.github/workflows/go-lint/action.yml deleted file mode 100644 index d041aa4627a..00000000000 --- a/.github/workflows/go-lint/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Go Lint -runs: - using: composite - steps: - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: 1.22.2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v4 - with: - version: v1.57.2 - working-directory: ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src - args: --config=${{ github.workspace }}/.golangci.yml diff --git a/.github/workflows/sdk-ci/action.yml b/.github/workflows/sdk-ci/action.yml index 3113b1b91b4..f20b59ee8d6 100644 --- a/.github/workflows/sdk-ci/action.yml +++ b/.github/workflows/sdk-ci/action.yml @@ -1,6 +1,20 @@ -name: Continuous Integration for SDKs +name: 'Run CI for Go SDK' runs: - using: composite + using: 'composite' steps: - - name: Lint for Go SDK - uses: ./.github/workflows/go-lint + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '^3.12' + + - name: Install dependencies for JsonRPC Server + shell: bash + run: | + python -m pip install --upgrade pip + cd ./ocaml/sdk-gen/component-test/jsonrpc-server + pip install -r requirements.txt + + - name: Run CI for Go SDK + uses: ./.github/workflows/go-ci + + # Run other tests here \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/README.md b/ocaml/sdk-gen/component-test/README.md new file mode 100644 index 00000000000..d60b597c25d --- /dev/null +++ b/ocaml/sdk-gen/component-test/README.md @@ -0,0 +1,67 @@ +## How Component Testing Works + +#### jsonrpc-server +The jsonrpc-server is a mock HTTP server which aligns with [jsonrpc 2.0 specification](https://www.jsonrpc.org/specification) and to emulate the xen-api server. It parses the test data from JSON files located in the spec/ directory, executes the method and parameters specified in spec files with incoming requests and then sends back a jsonrpc response that includes the expect_result. + +For the purpose of backwards and forwards compatibility, the following structure is recommended: + +- Base Directory: All test cases should be housed within a dedicated directory, conventionally named spec/. + +- Sub-directories: As the number of test cases grows or when differentiating between API versions, it becomes advantageous to categorize them into sub-directories. These sub-directories can represent different versions or modules of the API. +``` +spec/ +├── v1/ +│ ├── test_id_1.json +│ ├── test_id_2.json +│ └── ... +├── v2/ +│ ├── test_id_3.json +│ ├── test_id_4.json +│ └── ... +└── ... +``` + +- Test Data Specification: Within each JSON file, the test data should be structured to include essential fields such as test_id, method, params, and expect_result. This structure allows for clear definition and expectation of each test case. +```json +{ + "test_id": "test_id_1", + "method": "methodName", + "params": { + // ... parameters required for the method + + }, + "expect_result": { + // ... expected result of the method execution + + } + +} +``` + +#### jsonrpc-client +jsonrpc-client is a client that imports the SDK and runs the functions, following these important details: + +1. Add test_id as a customize request header. + +2. Ensure that the function and params are aligned with the data defined in spec/ directory. + +3. In order to support test reports, practitioners should use the specific test framework to test SDK, eg: pytest, gotest, junit, xUnit and so on. + +4. To support the SDK component test, it recommended to move the SDK generated to a sub directory as a local module for import purposes, eg: +``` +cp -r ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src jsonrpc-client/go/goSDK +``` +then, import the local module. +``` +module github.com/xapi-project/xen-api/sdk-gen/component-test/jsonrpc-client/go + +go 1.22.2 + +replace xenapi => ./goSDK +``` + +#### github actions +For a CI step in the generate sdk sources job, it should involve performing lint and component testing after sdk generation. + +## Run test locally +Install python 3.11+ with requirements and go 1.22+ and go to ocaml/sdk-gen/component-test and run `bash run-tests.sh` diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod b/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod new file mode 100644 index 00000000000..f1ee4c8ee52 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod @@ -0,0 +1,5 @@ +module github.com/xapi-project/xen-api/sdk-gen/component-test/jsonrpc-client/go + +go 1.22.2 + +replace xenapi => ./goSDK \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go new file mode 100644 index 00000000000..466c037deed --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "flag" + "fmt" + "testing" + "xenapi" +) + +const ServerURL = "http://localhost:5000" + +var session *xenapi.Session + +var USERNAME_FLAG = flag.String("root", "", "the username of the host (e.g. root)") +var PASSWORD_FLAG = flag.String("secret", "", "the password of the host") + +func TestLoginSuccess(t *testing.T) { + session = xenapi.NewSession(&xenapi.ClientOpts{ + URL: ServerURL, + Headers: map[string]string{ + "Test-ID": "test_id1", + }, + }) + if session == nil { + fmt.Printf("Failed to get the session") + return + } + _, err := session.LoginWithPassword(*USERNAME_FLAG, *PASSWORD_FLAG, "1.0", "Go sdk component test") + if err != nil { + t.Log(err) + t.Fail() + return + } + + expectedXapiVersion := "1.20" + getXapiVersion := session.XAPIVersion + if expectedXapiVersion != getXapiVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedXapiVersion, getXapiVersion) + } + var expectedAPIVersion xenapi.APIVersion = xenapi.APIVersion2_21 + getAPIVersion := session.APIVersion + if expectedAPIVersion != getAPIVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedAPIVersion, getAPIVersion) + } + + err = session.Logout() + if err != nil { + t.Log(err) + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt b/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt new file mode 100644 index 00000000000..e9feb68da3d --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt @@ -0,0 +1 @@ + aiohttp diff --git a/ocaml/sdk-gen/component-test/jsonrpc-server/server.py b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py new file mode 100644 index 00000000000..8fff806eac1 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py @@ -0,0 +1,72 @@ +""" +Module Name: + jsonrpc_server +Description: + This module provides a simple JSON-RPC server implementation using aiohttp. +""" + +import json +import os + +from aiohttp import web # pytype: disable=import-error + + +def load_json_files(): + """ + Load all JSON files from the 'spec' directory and merge their contents + into a dictionary. + + Returns: + dict: A dictionary containing the merged contents of all JSON files. + """ + data = {} + for filename in os.listdir("spec/"): + if filename.endswith(".json"): + with open(f"spec/{filename}", "r", encoding="utf-8") as f: + data.update(json.load(f)) + return data + + +async def handle(request): + """ + Handle incoming requests and execute methods based on the test ID provided + in the request headers. + + Args: + request (aiohttp.web.Request): The incoming HTTP request. + + Returns: + aiohttp.web.Response: The HTTP response containing the JSON-RPC result. + """ + spec = load_json_files() + test_id = request.headers.get("Test-ID") + test_data = spec.get(test_id, {}) + data = await request.json() + + try: + assert data.get("method") in test_data.get("method") + assert test_data.get("params")[data.get("method")] == data.get("params") + except Exception: + response = { + "jsonrpc": "2.0", + "id": data.get("id"), + "error": { + "code": 500, + "message": "Rpc server failed to handle the client request!", + "data": str(data), + } + } + else: + response = { + "jsonrpc": "2.0", + "id": data.get("id"), + **test_data.get("expected_result")[data.get("method")], + } + return web.json_response(response) + + +app = web.Application() +app.router.add_post("/jsonrpc", handle) + +if __name__ == "__main__": + web.run_app(app, port=5000) diff --git a/ocaml/sdk-gen/component-test/run-tests.sh b/ocaml/sdk-gen/component-test/run-tests.sh new file mode 100644 index 00000000000..0eed34f0e0c --- /dev/null +++ b/ocaml/sdk-gen/component-test/run-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -ex + +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) + +start_jsonrpc_server() { + echo "Starting JSONRPC server" + python3 jsonrpc-server/server.py & + JSONRPC_SERVER_PID=$! + sleep 1 +} + +start_jsonrpc_go_client() { + echo "Starting JSONRPC Go client" + + cd jsonrpc-client/go + # ensure that all dependencies are satisfied + go mod tidy + # build client.go and run it + go test main_test.go -v & + JSONRPC_GO_CLIENT_PID=$! +} + +trap 'kill $JSONRPC_SERVER_PID $JSONRPC_GO_CLIENT_PID 2>/dev/null' EXIT + +main() { + cd "$SCRIPT_PATH" + start_jsonrpc_server + start_jsonrpc_go_client + + # Wait for the component test to finish + wait $JSONRPC_GO_CLIENT_PID + + # Shut down the server to reduce future problems when testing other clients. + kill $JSONRPC_SERVER_PID +} + +main diff --git a/ocaml/sdk-gen/component-test/spec/session.json b/ocaml/sdk-gen/component-test/spec/session.json new file mode 100644 index 00000000000..4916448f3d9 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/session.json @@ -0,0 +1,40 @@ +{ + "test_id1": { + "method": [ + "session.login_with_password", + "pool.get_all", + "pool.get_record", + "host.get_record", + "session.logout" + ], + "params": { + "session.login_with_password": ["", "", "1.0", "Go sdk component test"], + "pool.get_all": ["login successfully"], + "pool.get_record": ["login successfully", "poolref0"], + "host.get_record": ["login successfully", ""], + "session.logout": ["login successfully"] + }, + "expected_result": { + "session.login_with_password": { + "result": "login successfully" + }, + "pool.get_all": { + "result": ["poolref0"] + }, + "pool.get_record": { + "result": {"name_label": "pool0"} + }, + "host.get_record": { + "result": { + "name_label": "host0", + "software_version": {"xapi": "1.20"}, + "API_version_major": "2", + "API_version_minor": "21" + } + }, + "session.logout": { + "result": "logout successfully" + } + } + } +} \ No newline at end of file From 5042fd92a0a8ab6863d1f669ac753d29f86d1849 Mon Sep 17 00:00:00 2001 From: minglumlu Date: Fri, 31 May 2024 11:12:23 +0100 Subject: [PATCH 52/99] Go SDK: Misc fixes for on-going component tests (#5661) * CP-49707: Go SDK: Add JSON tag for structs This aims to allow a Go SDK client to unmarshal JSON to Go structs with the function in Go json pacakge instead of using Go SDK. The unmarshalling in Go SDK contains two steps: JSON -> map -> struct. This is because some special cases need to be handled explicitly. But for sake of safety, this unmarshalling is not exposed to clients. Signed-off-by: Ming Lu * CP-49349: Go SDK: Append UTC timezone to unix timestamp The date time in Go requires a date time with timezone info. But XAPI may return a UNIX timestamp to clients. This commit blindly adds a UTC timezone for UNIX timestamps returned from XAPI server. Signed-off-by: Ming Lu * CP-49349: Go SDK: Allow spaces in special value of float type Some special values of float standing in JSON are handled specifically. This commit allows the special values contain spaces. Signed-off-by: Ming Lu * Go SDK: Disable golangci tagliatelle This check complains the struct json tag naming style. It expects the Caml style, but Go SDK has to use Snake. Signed-off-by: Ming Lu --------- Signed-off-by: Ming Lu --- .golangci.yml | 2 +- ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go | 10 +++++++--- ocaml/sdk-gen/go/gen_go_helper.ml | 2 ++ ocaml/sdk-gen/go/templates/ConvertTime.mustache | 4 ++-- ocaml/sdk-gen/go/templates/Record.mustache | 10 +++++----- ocaml/sdk-gen/go/test_data/record.go | 6 +++--- ocaml/sdk-gen/go/test_data/time_convert.go | 4 ++-- ocaml/sdk-gen/go/test_gen_go.ml | 9 +++++++-- 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6ddf8fa2994..17800bb7357 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -69,7 +69,6 @@ linters: - testpackage # makes you use a separate _test package - decorder # checks declaration order and count of types, constants, variables and functions - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers - - tagliatelle # checks the struct tags - godox # detects FIXME, TODO and other comment keywords - gci # controls golang package import order and makes it always deterministic - tagalign # checks that struct tags are well aligned @@ -78,6 +77,7 @@ linters: ## disabled + #- tagliatelle # checks the struct tags #- goconst # finds repeated strings that could be replaced by a constant #- gochecknoglobals # checks that no global variables exist #- gocyclo # computes and checks the cyclomatic complexity of functions diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go index b4dd2d601e5..09600a5bd68 100644 --- a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "os" + "regexp" "strings" "time" ) @@ -66,9 +67,12 @@ func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http func convertUnhandledJSONData(jsonBytes []byte) []byte { jsonString := string(jsonBytes) - jsonString = strings.ReplaceAll(jsonString, ":Infinity", ":\"+Inf\"") - jsonString = strings.ReplaceAll(jsonString, ":-Infinity", ":\"-Inf\"") - jsonString = strings.ReplaceAll(jsonString, ":NaN", ":\"NaN\"") + re := regexp.MustCompile(`:[ ]*-Infinity`) + jsonString = re.ReplaceAllString(jsonString, `:"-Inf"`) + re = regexp.MustCompile(`:[ +]*Infinity`) + jsonString = re.ReplaceAllString(jsonString, `:"+Inf"`) + re = regexp.MustCompile(`:[ ]*NaN`) + jsonString = re.ReplaceAllString(jsonString, `:"NaN"`) return []byte(jsonString) } diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 02144e89567..cefaaad854a 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -180,6 +180,7 @@ module Json = struct `O [ ("name", `String (concat_and_convert field)) + ; ("json_name", `String (String.concat "_" field.full_name)) ; ("description", `String (String.trim field.field_description)) ; ("type", `String ty) ] @@ -217,6 +218,7 @@ module Json = struct `O [ ("name", `String "Snapshot") + ; ("json_name", `String "snapshot") ; ( "description" , `String "The record of the database object that was added, changed or \ diff --git a/ocaml/sdk-gen/go/templates/ConvertTime.mustache b/ocaml/sdk-gen/go/templates/ConvertTime.mustache index d79f65841ad..d1f18643057 100644 --- a/ocaml/sdk-gen/go/templates/ConvertTime.mustache +++ b/ocaml/sdk-gen/go/templates/ConvertTime.mustache @@ -26,9 +26,9 @@ func deserialize{{func_name_suffix}}(context string, input interface{}) (value { return } unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) - value = time.Unix(unixTimestamp, 0) + value = time.Unix(unixTimestamp, 0).UTC() return } -{{/deserialize}} \ No newline at end of file +{{/deserialize}} diff --git a/ocaml/sdk-gen/go/templates/Record.mustache b/ocaml/sdk-gen/go/templates/Record.mustache index b30e234e1cb..8b10dc04ab7 100644 --- a/ocaml/sdk-gen/go/templates/Record.mustache +++ b/ocaml/sdk-gen/go/templates/Record.mustache @@ -1,7 +1,7 @@ type {{name}}Record struct { {{#fields}} //{{#description}} {{.}}{{/description}} - {{name}} {{type}} + {{name}} {{type}} `json:"{{json_name}},omitempty"` {{/fields}} } @@ -11,9 +11,9 @@ type {{name}}Ref string type RecordInterface interface{} type EventBatch struct { - Token string - ValidRefCounts map[string]int - Events []EventRecord + Token string `json:"token,omitempty"` + ValidRefCounts map[string]int `json:"validRefCounts,omitempty"` + Events []EventRecord `json:"events,omitempty"` } {{/event}} @@ -40,4 +40,4 @@ func NewSession(opts *ClientOpts) *Session { type {{name_internal}} struct{} var {{name}} {{name_internal}} -{{/session}} \ No newline at end of file +{{/session}} diff --git a/ocaml/sdk-gen/go/test_data/record.go b/ocaml/sdk-gen/go/test_data/record.go index c07e18b9fb9..b7ca793880c 100644 --- a/ocaml/sdk-gen/go/test_data/record.go +++ b/ocaml/sdk-gen/go/test_data/record.go @@ -1,8 +1,8 @@ type SessionRecord struct { // Unique identifier/object reference - UUID string + UUID string `json:"uuid,omitempty"` // Currently connected host - ThisHost HostRef + ThisHost HostRef `json:"thishost,omitempty"` } type SessionRef string @@ -21,4 +21,4 @@ func NewSession(opts *ClientOpts) *Session { session.client = client return &session -} \ No newline at end of file +} diff --git a/ocaml/sdk-gen/go/test_data/time_convert.go b/ocaml/sdk-gen/go/test_data/time_convert.go index d6da10f4d42..7bbdf602ced 100644 --- a/ocaml/sdk-gen/go/test_data/time_convert.go +++ b/ocaml/sdk-gen/go/test_data/time_convert.go @@ -23,7 +23,7 @@ func deserializeTime(context string, input interface{}) (value time.Time, err er return } unixTimestamp, err := strconv.ParseInt(strconv.Itoa(int(floatValue)), 10, 64) - value = time.Unix(unixTimestamp, 0) + value = time.Unix(unixTimestamp, 0).UTC() return -} \ No newline at end of file +} diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 8f161730fad..4c06f17d28f 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -58,12 +58,15 @@ let schema_check keys checker members = compare_keys keys keys' && List.for_all checker members let verify_field_member = function - | "name", `String _ | "description", `String _ | "type", `String _ -> + | "name", `String _ + | "description", `String _ + | "type", `String _ + | "json_name", `String _ -> true | _ -> false -let field_keys = ["name"; "description"; "type"] +let field_keys = ["name"; "description"; "type"; "json_name"] let verify_field = function | `O members -> @@ -558,12 +561,14 @@ let record : Mustache.Json.t = `O [ ("name", `String "UUID") + ; ("json_name", `String "uuid") ; ("description", `String "Unique identifier/object reference") ; ("type", `String "string") ] ; `O [ ("name", `String "ThisHost") + ; ("json_name", `String "thishost") ; ("description", `String "Currently connected host") ; ("type", `String "HostRef") ] From 2e7e31855c3b76966518b53428e12142d7957339 Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Fri, 17 May 2024 14:55:52 +0100 Subject: [PATCH 53/99] doc: copy design documents from xapi-project.github.io Signed-off-by: Rob Hoes --- doc/content/design/RDP.md | 98 ++ doc/content/design/_index.md | 6 + doc/content/design/aggr-storage-reboots.md | 67 ++ doc/content/design/archival-redesign.md | 95 ++ doc/content/design/backtraces.md | 298 ++++++ doc/content/design/bonding-improvements.md | 288 ++++++ .../design/coverage/coverage-screenshot.png | Bin 0 -> 111622 bytes doc/content/design/coverage/index.md | 267 ++++++ doc/content/design/cpu-levelling-v2.md | 202 ++++ .../distributed-database/architecture.png | Bin 0 -> 89659 bytes .../design/distributed-database/index.md | 181 ++++ .../design/distributed-database/topic.png | Bin 0 -> 55724 bytes doc/content/design/emergency-network-reset.md | 143 +++ doc/content/design/emulated-pci-spec.md | 61 ++ doc/content/design/fcoe-nics.md | 56 ++ doc/content/design/gpu-passthrough.md | 365 ++++++++ doc/content/design/gpu-support-evolution.md | 209 +++++ doc/content/design/heterogeneous-pools.md | 289 ++++++ .../integrated-gpu-passthrough/index.md | 95 ++ .../integrated-gpu-passthrough.png | Bin 0 -> 20590 bytes doc/content/design/local-database.md | 68 ++ .../design/management-interface-on-vlan.md | 224 +++++ .../design/multiple-cluster-managers.md | 73 ++ .../design/multiple-device-emulators.md | 72 ++ doc/content/design/ocfs2/index.md | 491 ++++++++++ .../design/ocfs2/o2cb-enable-external.msc | 13 + .../design/ocfs2/o2cb-enable-external.svg | 15 + .../design/ocfs2/o2cb-enable-internal1.msc | 14 + .../design/ocfs2/o2cb-enable-internal1.svg | 15 + .../design/ocfs2/o2cb-enable-internal2.msc | 16 + .../design/ocfs2/o2cb-enable-internal2.svg | 17 + doc/content/design/ocfs2/ocfs2.graffle | Bin 0 -> 3640 bytes doc/content/design/ocfs2/ocfs2.png | Bin 0 -> 49597 bytes doc/content/design/ocfs2/pool-eject.msc | 13 + doc/content/design/ocfs2/pool-eject.svg | 13 + doc/content/design/patches-in-vdis.md | 77 ++ doc/content/design/pci-passthrough.md | 76 ++ doc/content/design/pif-properties.md | 64 ++ doc/content/design/plugin-protocol-v2.md | 198 ++++ doc/content/design/plugin-protocol-v3.md | 81 ++ doc/content/design/pool-wide-ssh.md | 80 ++ doc/content/design/schedule-snapshot.md | 89 ++ doc/content/design/smapiv3/index.md | 95 ++ doc/content/design/smapiv3/plugin.graffle | Bin 0 -> 3429 bytes doc/content/design/smapiv3/plugin.png | Bin 0 -> 56039 bytes doc/content/design/smapiv3/smapiv3.graffle | Bin 0 -> 5269 bytes doc/content/design/smapiv3/smapiv3.png | Bin 0 -> 59652 bytes doc/content/design/snapshot-revert.md | 103 ++ doc/content/design/sr-level-rrds.md | 147 +++ .../design/thin-lvhd/allocation-plane.graffle | Bin 0 -> 5149 bytes .../design/thin-lvhd/allocation-plane.png | Bin 0 -> 84034 bytes .../design/thin-lvhd/control-plane.graffle | Bin 0 -> 4422 bytes .../design/thin-lvhd/control-plane.png | Bin 0 -> 67359 bytes doc/content/design/thin-lvhd/index.md | 878 ++++++++++++++++++ doc/content/design/thin-lvhd/queue.pml | 64 ++ .../design/thin-lvhd/thin-lvhd.graffle | Bin 0 -> 7315 bytes doc/content/design/thin-lvhd/thin-lvhd.png | Bin 0 -> 67719 bytes doc/content/design/thin-lvhd/xenvmd.graffle | Bin 0 -> 5667 bytes doc/content/design/tunnelling.md | 197 ++++ doc/content/design/vgpu-type-identifiers.md | 112 +++ doc/content/design/virt-hw-platform-vn.md | 39 + doc/content/design/xenopsd_events.md | 47 + doc/content/design/xenprep.md | 79 ++ 63 files changed, 6190 insertions(+) create mode 100644 doc/content/design/RDP.md create mode 100644 doc/content/design/_index.md create mode 100644 doc/content/design/aggr-storage-reboots.md create mode 100644 doc/content/design/archival-redesign.md create mode 100644 doc/content/design/backtraces.md create mode 100644 doc/content/design/bonding-improvements.md create mode 100644 doc/content/design/coverage/coverage-screenshot.png create mode 100644 doc/content/design/coverage/index.md create mode 100644 doc/content/design/cpu-levelling-v2.md create mode 100644 doc/content/design/distributed-database/architecture.png create mode 100644 doc/content/design/distributed-database/index.md create mode 100644 doc/content/design/distributed-database/topic.png create mode 100644 doc/content/design/emergency-network-reset.md create mode 100644 doc/content/design/emulated-pci-spec.md create mode 100644 doc/content/design/fcoe-nics.md create mode 100644 doc/content/design/gpu-passthrough.md create mode 100644 doc/content/design/gpu-support-evolution.md create mode 100644 doc/content/design/heterogeneous-pools.md create mode 100644 doc/content/design/integrated-gpu-passthrough/index.md create mode 100644 doc/content/design/integrated-gpu-passthrough/integrated-gpu-passthrough.png create mode 100644 doc/content/design/local-database.md create mode 100644 doc/content/design/management-interface-on-vlan.md create mode 100644 doc/content/design/multiple-cluster-managers.md create mode 100644 doc/content/design/multiple-device-emulators.md create mode 100644 doc/content/design/ocfs2/index.md create mode 100644 doc/content/design/ocfs2/o2cb-enable-external.msc create mode 100644 doc/content/design/ocfs2/o2cb-enable-external.svg create mode 100644 doc/content/design/ocfs2/o2cb-enable-internal1.msc create mode 100644 doc/content/design/ocfs2/o2cb-enable-internal1.svg create mode 100644 doc/content/design/ocfs2/o2cb-enable-internal2.msc create mode 100644 doc/content/design/ocfs2/o2cb-enable-internal2.svg create mode 100644 doc/content/design/ocfs2/ocfs2.graffle create mode 100644 doc/content/design/ocfs2/ocfs2.png create mode 100644 doc/content/design/ocfs2/pool-eject.msc create mode 100644 doc/content/design/ocfs2/pool-eject.svg create mode 100644 doc/content/design/patches-in-vdis.md create mode 100644 doc/content/design/pci-passthrough.md create mode 100644 doc/content/design/pif-properties.md create mode 100644 doc/content/design/plugin-protocol-v2.md create mode 100644 doc/content/design/plugin-protocol-v3.md create mode 100644 doc/content/design/pool-wide-ssh.md create mode 100644 doc/content/design/schedule-snapshot.md create mode 100644 doc/content/design/smapiv3/index.md create mode 100644 doc/content/design/smapiv3/plugin.graffle create mode 100644 doc/content/design/smapiv3/plugin.png create mode 100644 doc/content/design/smapiv3/smapiv3.graffle create mode 100644 doc/content/design/smapiv3/smapiv3.png create mode 100644 doc/content/design/snapshot-revert.md create mode 100644 doc/content/design/sr-level-rrds.md create mode 100644 doc/content/design/thin-lvhd/allocation-plane.graffle create mode 100644 doc/content/design/thin-lvhd/allocation-plane.png create mode 100644 doc/content/design/thin-lvhd/control-plane.graffle create mode 100644 doc/content/design/thin-lvhd/control-plane.png create mode 100644 doc/content/design/thin-lvhd/index.md create mode 100644 doc/content/design/thin-lvhd/queue.pml create mode 100644 doc/content/design/thin-lvhd/thin-lvhd.graffle create mode 100644 doc/content/design/thin-lvhd/thin-lvhd.png create mode 100644 doc/content/design/thin-lvhd/xenvmd.graffle create mode 100644 doc/content/design/tunnelling.md create mode 100644 doc/content/design/vgpu-type-identifiers.md create mode 100644 doc/content/design/virt-hw-platform-vn.md create mode 100644 doc/content/design/xenopsd_events.md create mode 100644 doc/content/design/xenprep.md diff --git a/doc/content/design/RDP.md b/doc/content/design/RDP.md new file mode 100644 index 00000000000..401e6642b38 --- /dev/null +++ b/doc/content/design/RDP.md @@ -0,0 +1,98 @@ +--- +title: RDP control +layout: default +design_doc: true +revision: 2 +status: released (XenServer 6.5 SP1) +design_review: 12 +--- +### Purpose + +To administer guest VMs it can be useful to connect to them over Remote Desktop Protocol (RDP). XenCenter supports this; it has an integrated RDP client. + +First it is necessary to turn on the RDP service in the guest. + +This can be controlled from XenCenter. Several layers are involved. This description starts in the guest and works up the stack to XenCenter. + +This feature was completed in the first quarter of 2015, and released in Service Pack 1 for XenServer 6.5. + +### The guest agent + +The XenServer guest agent installed in Windows VMs can turn the RDP service on and off, and can report whether it is running. + +The guest agent is at https://github.com/xenserver/win-xenguestagent + +Interaction with the agent is done through some Xenstore keys: + +The guest agent running in domain N writes two xenstore nodes when it starts up: +* `/local/domain/N/control/feature-ts = 1` +* `/local/domain/N/control/feature-ts2 = 1` + +This indicates support for the rest of the functionality described below. + +(The "...ts2" flag is new for this feature; older versions of the guest agent wrote the "...ts" flag and had support for only a subset of the functionality (no firewall modification), and had a bug in updating `.../data/ts`.) + +To indicate whether RDP is running, the guest agent writes the string "1" (running) or "0" (disabled) to xenstore node + +`/local/domain/N/data/ts`. + +It does this on start-up, and also in response to the deletion of that node. + +The guest agent also watches xenstore node `/local/domain/N/control/ts` and it turns RDP on and off in response to "1" or "0" (respectively) being written to that node. The agent acknowledges the request by deleting the node, and afterwards it deletes `local/domain/N/data/ts`, thus triggering itself to update that node as described above. + +When the guest agent turns the RDP service on/off, it also modifies the standard Windows firewall to allow/forbid incoming connections to the RDP port. This is the same as the firewall change that happens automatically when the RDP service is turned on/off through the standard Windows GUI. + +### XAPI etc. + +xenopsd sets up watches on xenstore nodes including the `control` tree and `data/ts`, and prompts xapi to react by updating the relevant VM guest metrics record, which is available through a XenAPI call. + +XenAPI includes a new message (function call) which can be used to ask the guest agent to turn RDP on and off. + +This is `VM.call_plugin` (analogous to `Host.call_plugin`) in the hope that it can be used for other purposes in the future, even though for now it does not really call a plugin. + +To use it, supply `plugin="guest-agent-operation"` and either `fn="request_rdp_on"` or `fn="request_rdp_off"`. + +See http://xapi-project.github.io/xen-api/classes/vm.html + +The function strings are named with "request" (rather than, say, "enable_rdp" or "turn_rdp_on") to make it clear that xapi only makes a request of the guest: when one of these calls returns successfully this means only that the appropriate string (1 or 0) was written to the `control/ts` node and it is up to the guest whether it responds. + +### XenCenter + +#### Behaviour on older XenServer versions that do not support RDP control + +Note that the current behaviour depends on some global options: "Enable Remote Desktop console scanning" and "Automatically switch to the Remote Desktop console when it becomes available". + +1. When tools are not installed: + * As of XenCenter 6.5, the RDP button is absent. +2. When tools are installed but RDP is not switched on in the guest: + 1. If "Enable Remote Desktop console scanning" is on: + * The RDP button is present but greyed out. (It seems to sometimes read "Switch to Remote Desktop" and sometimes read "Looking for guest console...": I haven't yet worked out the difference). + * We scan the RDP port to detect when RDP is turned on + 2. If "Enable Remote Desktop console scanning" is off: + * The RDP button is enabled and reads "Switch to Remote Desktop" +3. When tools are installed and RDP is switched on in the guest: + 1. If "Enable Remote Desktop console scanning" is on: + * The RDP button is enabled and reads "Switch to Remote Desktop" + * If "Automatically switch" is on, we switch to RDP immediately we detect it + 2. If "Enable Remote Desktop console scanning" is off: + * As above, the RDP button is enabled and reads "Switch to Remote Desktop" + +#### New behaviour on XenServer versions that support RDP control + +1. This new XenCenter behaviour is only for XenServer versions that support RDP control, with guests with the new guest agent: behaviour must be unchanged if the server or guest-agent is older. +2. There should be no change in the behaviour for Linux guests, either PV or HVM varieties: this must be tested. +3. We should never scan the RDP port; instead we should watch for a change in the relevant variable in guest_metrics. +4. The XenCenter option "Enable Remote Desktop console scanning" should change to read "Enable Remote Desktop console scanning (XenServer 6.5 and earlier)" +5. The XenCenter option "Automatically switch to the Remote Desktop console when it becomes available" should be enabled even when "Enable Remote Desktop console scanning" is off. +6. When tools are not installed: + * As above, the RDP button should be absent. +7. When tools are installed but RDP is not switched on in the guest: + * The RDP button should be enabled and read "Turn on Remote Desktop" + * If pressed, it should launch a dialog with the following wording: "Would you like to turn on Remote Desktop in this VM, and then connect to it over Remote Desktop? [Yes] [No]" + * That button should turn on RDP, wait for RDP to become enabled, and switch to an RDP connection. It should do this even if "Automatically switch" is off. +8. When tools are installed and RDP is switched on in the guest: + * The RDP button should be enabled and read "Switch to Remote Desktop" + * If "Automatically switch" is on, we should switch to RDP immediately + * There is no need for us to provide UI to switch RDP off again +9. We should also test the case where RDP has been switched on in the guest before the tools are installed. + diff --git a/doc/content/design/_index.md b/doc/content/design/_index.md new file mode 100644 index 00000000000..59f76755101 --- /dev/null +++ b/doc/content/design/_index.md @@ -0,0 +1,6 @@ ++++ +title = "Design Documents" +menuTitle = "Designs" ++++ + +{{% children %}} diff --git a/doc/content/design/aggr-storage-reboots.md b/doc/content/design/aggr-storage-reboots.md new file mode 100644 index 00000000000..b3173f6e19e --- /dev/null +++ b/doc/content/design/aggr-storage-reboots.md @@ -0,0 +1,67 @@ +--- +title: Aggregated Local Storage and Host Reboots +layout: default +design_doc: true +revision: 3 +status: proposed +design_review: 144 +revision_history: +- revision_number: 1 + description: Initial version +- revision_number: 2 + description: Included some open questions under Xapi point 2 +- revision_number: 3 + description: Added new error, task, and assumptions +--- + +## Introduction + +When hosts use an aggregated local storage SR, then disks are going to be mirrored to several different hosts in the pool (RAID). This ensures that if a host goes down (e.g. due to a reboot after installing a hotfix or upgrade, or when "fenced" by the HA feature), all disk contents in the SR are still accessible. This also means that if all disks are mirrored to just two hosts (worst-case scenario), just one host may be down at any point in time to keep the SR fully available. + +When a node comes back up after a reboot, it will resynchronise all its disks with the related mirrors on the other hosts in the pool. This syncing takes some time, and only after this is done, we may consider the host "up" again, and allow another host to be shut down. + +Therefore, when installing a hotfix to a pool that uses aggregated local storage, or doing a rolling pool upgrade, we need to make sure that we do hosts one-by-one, and we wait for the storage syncing to finish before doing the next. + +This design aims to provide guidance and protection around this by blocking hosts to be shut down or rebooted from the XenAPI except when safe, and setting the `host.allowed_operations` field accordingly. + + +## XenAPI + +If an aggregated local storage SR is in use, and one of the hosts is rebooting or down (for whatever reason), or resynchronising its storage, the operations `reboot` and `shutdown` will be removed from the `host.allowed_operations` field of _all_ hosts in the pool that have a PBD for the SR. + +This is a conservative approach in that assumes that this kind of SR tolerates only one node "failure", and assumes no knowledge about how the SR distributes its mirrors. We may refine this in future, in order to allow some hosts to be down simultaneously. + +The presence of the `reboot` operation in `host.allowed_operations` indicates whether the `host.reboot` XenAPI call is allowed or not (similarly for `shutdown` and `host.shutdown`). It will not, of course, prevent anyone from rebooting a host from the dom0 console or power switch. + +Clients, such as XenCenter, can use `host.allowed_operations`, when applying an update to a pool, to guide them when it is safe to update and reboot the next host in the sequence. + +In case `host.reboot` or `host.shutdown` is called while the storage is busy resyncing mirrors, the call will fail with a new error `MIRROR_REBUILD_IN_PROGRESS`. + +## Xapi + +Xapi needs to be able to: + +1. Determine whether aggregated local storage is in use; this just means that a PBD for such an SR present. + * TBD: To avoid SR-specific code in xapi, the storage backend should tell us whether it is an aggregated local storage SR. +2. Determine whether the storage system is resynchronising its mirrors; it will need to be able to query the storage backend for this kind of information. + * Xapi will poll for this and will reflect that a resync is happening by creating a `Task` for it (in the DB). This task can be used to track progress, if available. + * The exact way to get the syncing information from the storage backend is SR specific. The check may be implemented in a separate script or binary that xapi calls from the polling thread. Ideally this would be integrated with the storage backend. +3. Update `host.allowed_operations` for all hosts in the pool according to the rules described above. This comes down to updating the function `valid_operations` in `xapi_host_helpers.ml`, and will need to use a combination of the functionality from the two points above, plus and indication of host liveness from `host_metrics.live`. +4. Trigger an update of the allowed operations when a host shuts down or reboots (due to a XenAPI call or otherwise), and when it has finished resynchronising when back up. Triggers must be in the following places (some may already be present, but are listed for completeness, and to confirm this): + * Wherever `host_metrics.live` is updated to detect pool slaves going up and down (probably at least in `Db_gc.check_host_liveness` and `Xapi_ha`). + * Immediately when a `host.reboot` or `host.shutdown` call is executed: `Message_forwarding.Host.{reboot,shutdown,with_host_operation}`. + * When a storage resync is starting or finishing. + +All of the above runs on the pool master (= SR master) only. + +## Assumptions + +The above will be safe if the storage cluster is equal to the XenServer pool. In general, however, it may be desirable to have a storage cluster that is larger than the pool, have multiple XS pools on a single cluster, or even share the cluster with other kinds of nodes. + +To ensure that the storage is "safe" in these scenarios, xapi needs to be able to ask the storage backend: + +1. if a mirror is being rebuilt "somewhere" in the cluster, AND +2. if "some node" in the cluster is offline (even if the node is not in the XS pool). + +If the cluster is equal to the pool, then xapi can do point 2 without asking the storage backend, which will simplify things. For the moment, we assume that the storage cluster is equal to the XS pool, to avoid making things too complicated (while still need to keep in mind that we may change this in future). + diff --git a/doc/content/design/archival-redesign.md b/doc/content/design/archival-redesign.md new file mode 100644 index 00000000000..34a3b898019 --- /dev/null +++ b/doc/content/design/archival-redesign.md @@ -0,0 +1,95 @@ +--- +title: RRDD archival redesign +layout: default +design_doc: true +revision: 1 +status: released (7,0) +--- + +## Introduction + +Current problems with rrdd: + +* rrdd stores knowledge about whether it is running on a master or a slave + +This determines the host to which rrdd will archive a VM's rrd when the VM's +domain disappears - rrdd will always try to archive to the master. However, +when a host joins a pool as a slave rrdd is not restarted so this knowledge is +out of date. When a VM shuts down on the slave rrdd will archive the rrd +locally. When starting this VM again the master xapi will attempt to push any +locally-existing rrd to the host on which the VM is being started, but since +no rrd archive exists on the master the slave rrdd will end up creating a new +rrd and the previous rrd will be lost. + +* rrdd handles rebooting VMs unpredictably + +When rebooting a VM, there is a chance rrdd will attempt to update that VM's rrd +during the brief period when there is no domain for that VM. If this happens, +rrdd will archive the VM's rrd to the master, and then create a new rrd for the +VM when it sees the new domain. If rrdd doesn't attempt to update that VM's rrd +during this period, rrdd will continue to add data for the new domain to the old +rrd. + +## Proposal + +To solve these problems, we will remove some of the intelligence from rrdd and +make it into more of a slave process of xapi. This will entail removing all +knowledge from rrdd of whether it is running on a master or a slave, and also +modifying rrdd to only start monitoring a VM when it is told to, and only +archiving an rrd (to a specified address) when it is told to. This matches the +way xenopsd only manages domains which it has been told to manage. + +## Design + +For most VM lifecycle operations, xapi and rrdd processes (sometimes across more +than one host) cooperate to start or stop recording a VM's metrics and/or to +restore or backup the VM's archived metrics. Below we will describe, for each +relevant VM operation, how the VM's rrd is currently handled, and how we propose +it will be handled after the redesign. + +#### VM.destroy + +The master xapi makes a remove_rrd call to the local rrdd, which causes rrdd to +to delete the VM's archived rrd from disk. This behaviour will remain unchanged. + +#### VM.start(\_on) and VM.resume(\_on) + +The master xapi makes a push_rrd call to the local rrdd, which causes rrdd to +send any locally-archived rrd for the VM in question to the rrdd of the host on +which the VM is starting. This behaviour will remain unchanged. + +#### VM.shutdown and VM.suspend + +Every update cycle rrdd compares its list of registered VMs to the list of +domains actually running on the host. Any registered VMs which do not have a +corresponding domain have their rrds archived to the rrdd running on the host +believed to be the master. We will change this behaviour by stopping rrdd from +doing the archiving itself; instead we will expose a new function in rrdd's +interface: + +``` +val archive_rrd : vm_uuid:string -> remote_address:string -> unit +``` + +This will cause rrdd to remove the specified rrd from its table of registered +VMs, and archive the rrd to the specified host. When a VM has finished shutting +down or suspending, the xapi process on the host on which the VM was running +will call archive_rrd to ask the local rrdd to archive back to the master rrdd. + +#### VM.reboot + +Removing rrdd's ability to automatically archive the rrds for disappeared +domains will have the bonus effect of fixing how the rrds of rebooting VMs are +handled, as we don't want the rrds of rebooting VMs to be archived at all. + +#### VM.checkpoint + +This will be handled automatically, as internally VM.checkpoint carries out a +VM.suspend followed by a VM.resume. + +#### VM.pool_migrate and VM.migrate_send + +The source host's xapi makes a migrate_rrd call to the local rrd, with a +destination address and an optional session ID. The session ID is only required +for cross-pool migration. The local rrdd sends the rrd for that VM to the +destination host's rrdd as an HTTP PUT. This behaviour will remain unchanged. diff --git a/doc/content/design/backtraces.md b/doc/content/design/backtraces.md new file mode 100644 index 00000000000..f8374be0d46 --- /dev/null +++ b/doc/content/design/backtraces.md @@ -0,0 +1,298 @@ +--- +title: Backtrace support +layout: default +design_doc: true +revision: 1 +status: Confirmed +--- + +We want to make debugging easier by recording exception backtraces which are + +- reliable +- cross-process (e.g. xapi to xenopsd) +- cross-language +- cross-host (e.g. master to slave) + +We therefore need + +- to ensure that backtraces are captured in our OCaml and python code +- a marshalling format for backtraces +- conventions for storing and retrieving backtraces + +Backtraces in OCaml +=================== + +OCaml has fast exceptions which can be used for both + +- control flow i.e. fast jumps from inner scopes to outer scopes +- reporting errors to users (e.g. the toplevel or an API user) + +To keep the exceptions fast, exceptions and backtraces are decoupled: +there is a single active backtrace per-thread at any one time. If you +have caught an exception and then throw another exception, the backtrace +buffer will be reinitialised, destroying your previous records. For example +consider a 'finally' function: + +```ocaml +let finally f cleanup = + try + let result = f () in + cleanup (); + result + with e -> + cleanup (); + raise e (* <-- backtrace starts here now *) +``` + +This function performs some action (i.e. `f ()`) and guarantees to +perform some cleanup action (`cleanup ()`) whether or not an exception +is thrown. This is a common pattern to ensure resources are freed (e.g. +closing a socket or file descriptor). Unfortunately the `raise e` in +the exception handler loses the backtrace context: when the exception +gets to the toplevel, `Printexc.get_backtrace ()` will point at the +`finally` rather than the real cause of the error. + +We will use a variant of the solution proposed by +[Jacques-Henri Jourdan](http://gallium.inria.fr/blog/a-library-to-record-ocaml-backtraces/) +where we will record backtraces when we catch exceptions, before the +buffer is reinitialised. Our `finally` function will now look like this: + +```ocaml +let finally f cleanup = + try + let result = f () in + cleanup (); + result + with e -> + Backtrace.is_important e; + cleanup (); + raise e +``` + +The function `Backtrace.is_important e` associates the exception `e` +with the current backtrace before it gets deleted. + +Xapi always has high-level exception handlers or other wrappers around all the +threads it spawns. In particular Xapi tries really hard to associate threads +with active tasks, so it can prefix all log lines with a task id. This helps +admins see the related log lines even when there is lots of concurrent activity. +Xapi also tries very hard to label other threads with names for the same reason +(e.g. `db_gc`). Every thread should end up being wrapped in `with_thread_named` +which allows us to catch exceptions and log stacktraces from `Backtrace.get` +on the way out. + +OCaml design guidelines +----------------------- + +Making nice backtraces requires us to think when we write our exception raising +and handling code. In particular: + +- If a function handles an exception and re-raise it, you must call + `Backtrace.is_important e` with the exception to capture the backtrace first. +- If a function raises a different exception (e.g. `Not_found` becoming a XenAPI + `INTERNAL_ERROR`) then you must use `Backtrace.reraise ` to + ensure the backtrace is preserved. +- All exceptions should be printable -- if the generic printer doesn't do a good + enough job then register a custom printer. +- If you are the last person who will see an exception (because you aren't going + to rethrow it) then you *may* log the backtrace via `Debug.log_backtrace e` + *if and only if* you reasonably expect the resulting backtrace to be helpful + and not spammy. +- If you aren't the last person who will see an exception (because you are going + to rethrow it or another exception), then *do not* log the backtrace; the + next handler will do that. +- All threads should have a final exception handler at the outermost level + for example `Debug.with_thread_named` will do this for you. + + +Backtraces in python +==================== + +Python exceptions behave similarly to the OCaml ones: if you raise a new +exception while handling an exception, the backtrace buffer is overwritten. +Therefore the same considerations apply. + +Python design guidelines +------------------------ + +The function [sys.exc_info()](https://docs.python.org/2/library/sys.html#sys.exc_info) +can be used to capture the traceback associated with the last exception. +We must guarantee to call this before constructing another exception. In +particular, this does not work: + +```python + raise MyException(sys.exc_info()) +``` + +Instead you must capture the traceback first: + +```python + exc_info = sys.exc_info() + raise MyException(exc_info) +``` + +Marshalling backtraces +====================== + +We need to be able to take an exception thrown from python code, gather +the backtrace, transmit it to an OCaml program (e.g. xenopsd) and glue +it onto the end of the OCaml backtrace. We will use a simple json marshalling +format for the raw backtrace data consisting of + +- a string summary of the error (e.g. an exception name) +- a list of filenames +- a corresponding list of lines + +(Note we don't use the more natural list of pairs as this confuses the +"rpclib" code generating library) + +In python: + +```python + results = { + "error": str(s[1]), + "files": files, + "lines": lines, + } + print json.dumps(results) +``` + +In OCaml: + +```ocaml + type error = { + error: string; + files: string list; + lines: int list; + } with rpc + print_string (Jsonrpc.to_string (rpc_of_error ...)) +``` + +Retrieving backtraces +===================== + +Backtraces will be written to syslog as usual. However it will also be +possible to retrieve the information via the CLI to allow diagnostic +tools to be written more easily. + +The CLI +------- + +We add a global CLI argument "--trace" which requests the backtrace be +printed, if one is available: + +``` +# xe vm-start vm=hvm --trace +Error code: SR_BACKEND_FAILURE_202 +Error parameters: , General backend error [opterr=exceptions must be old-style classes or derived from BaseException, not str], +Raised Server_error(SR_BACKEND_FAILURE_202, [ ; General backend error [opterr=exceptions must be old-style classes or derived from BaseException, not str]; ]) +Backtrace: +0/50 EXT @ st30 Raised at file /opt/xensource/sm/SRCommand.py, line 110 +1/50 EXT @ st30 Called from file /opt/xensource/sm/SRCommand.py, line 159 +2/50 EXT @ st30 Called from file /opt/xensource/sm/SRCommand.py, line 263 +3/50 EXT @ st30 Called from file /opt/xensource/sm/blktap2.py, line 1486 +4/50 EXT @ st30 Called from file /opt/xensource/sm/blktap2.py, line 83 +5/50 EXT @ st30 Called from file /opt/xensource/sm/blktap2.py, line 1519 +6/50 EXT @ st30 Called from file /opt/xensource/sm/blktap2.py, line 1567 +7/50 EXT @ st30 Called from file /opt/xensource/sm/blktap2.py, line 1065 +8/50 EXT @ st30 Called from file /opt/xensource/sm/EXTSR.py, line 221 +9/50 xenopsd-xc @ st30 Raised by primitive operation at file "lib/storage.ml", line 32, characters 3-26 +10/50 xenopsd-xc @ st30 Called from file "lib/task_server.ml", line 176, characters 15-19 +11/50 xenopsd-xc @ st30 Raised at file "lib/task_server.ml", line 184, characters 8-9 +12/50 xenopsd-xc @ st30 Called from file "lib/storage.ml", line 57, characters 1-156 +13/50 xenopsd-xc @ st30 Called from file "xc/xenops_server_xen.ml", line 254, characters 15-63 +14/50 xenopsd-xc @ st30 Called from file "xc/xenops_server_xen.ml", line 1643, characters 15-76 +15/50 xenopsd-xc @ st30 Called from file "lib/xenctrl.ml", line 127, characters 13-17 +16/50 xenopsd-xc @ st30 Re-raised at file "lib/xenctrl.ml", line 127, characters 56-59 +17/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 937, characters 3-54 +18/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 1103, characters 4-71 +19/50 xenopsd-xc @ st30 Called from file "list.ml", line 84, characters 24-34 +20/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 1098, characters 2-367 +21/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 1203, characters 3-46 +22/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 1441, characters 3-9 +23/50 xenopsd-xc @ st30 Raised at file "lib/xenops_server.ml", line 1452, characters 9-10 +24/50 xenopsd-xc @ st30 Called from file "lib/xenops_server.ml", line 1458, characters 48-60 +25/50 xenopsd-xc @ st30 Called from file "lib/task_server.ml", line 151, characters 15-26 +26/50 xapi @ st30 Raised at file "xapi_xenops.ml", line 1719, characters 11-14 +27/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +28/50 xapi @ st30 Raised at file "xapi_xenops.ml", line 2005, characters 13-14 +29/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +30/50 xapi @ st30 Raised at file "xapi_xenops.ml", line 1785, characters 15-16 +31/50 xapi @ st30 Called from file "message_forwarding.ml", line 233, characters 25-44 +32/50 xapi @ st30 Called from file "message_forwarding.ml", line 915, characters 15-67 +33/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +34/50 xapi @ st30 Raised at file "lib/pervasiveext.ml", line 26, characters 9-12 +35/50 xapi @ st30 Called from file "message_forwarding.ml", line 1205, characters 21-199 +36/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +37/50 xapi @ st30 Raised at file "lib/pervasiveext.ml", line 26, characters 9-12 +38/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +9/50 xapi @ st30 Raised at file "rbac.ml", line 236, characters 10-15 +40/50 xapi @ st30 Called from file "server_helpers.ml", line 75, characters 11-41 +41/50 xapi @ st30 Raised at file "cli_util.ml", line 78, characters 9-12 +42/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +43/50 xapi @ st30 Raised at file "lib/pervasiveext.ml", line 26, characters 9-12 +44/50 xapi @ st30 Called from file "cli_operations.ml", line 1889, characters 2-6 +45/50 xapi @ st30 Re-raised at file "cli_operations.ml", line 1898, characters 10-11 +46/50 xapi @ st30 Called from file "cli_operations.ml", line 1821, characters 14-18 +47/50 xapi @ st30 Called from file "cli_operations.ml", line 2109, characters 7-526 +48/50 xapi @ st30 Called from file "xapi_cli.ml", line 113, characters 18-56 +49/50 xapi @ st30 Called from file "lib/pervasiveext.ml", line 22, characters 3-9 +``` + +One can automatically set "--trace" for a whole shell session as follows: + +```bash +export XE_EXTRA_ARGS="--trace" +``` + +The XenAPI +---------- + +We already store error information in the XenAPI "Task" object and so we +can store backtraces at the same time. We shall add a field "backtrace" +which will have type "string" but which will contain s-expression encoded +backtrace data. Clients should not attempt to parse this string: its +contents may change in future. The reason it is different from the json +mentioned before is that it also contains host and process information +supplied by Xapi, and may be extended in future to contain other diagnostic +information. + + +The Xenopsd API +--------------- + +We already store error information in the xenopsd API "Task" objects, +we can extend these to store the backtrace in an additional field ("backtrace"). +This field will have type "string" but will contain s-expression encoded +backtrace data. + + +The SMAPIv1 API +--------------- + +Errors in SMAPIv1 are returned as XMLRPC "Faults" containing a code and +a status line. Xapi transforms these into XenAPI exceptions usually of the +form `SR_BACKEND_FAILURE_`. We can extend the SM backends to use the +XenAPI exception type directly: i.e. to marshal exceptions as dictionaries: + +```python + results = { + "Status": "Failure", + "ErrorDescription": [ code, param1, ..., paramN ] + } +``` + +We can then define a new backtrace-carrying error: + +- code = `SR_BACKEND_FAILURE_WITH_BACKTRACE` +- param1 = json-encoded backtrace +- param2 = code +- param3 = reason + +which is internally transformed into `SR_BACKEND_FAILURE_` and +the backtrace is appended to the current Task backtrace. From the client's +point of view the final exception should look the same, but Xapi will have +a chance to see and log the whole backtrace. + +As a side-effect, it is possible for SM plugins to throw XenAPI errors directly, +without interpretation by Xapi. diff --git a/doc/content/design/bonding-improvements.md b/doc/content/design/bonding-improvements.md new file mode 100644 index 00000000000..fc09a8a7fe1 --- /dev/null +++ b/doc/content/design/bonding-improvements.md @@ -0,0 +1,288 @@ +--- +title: Bonding Improvements design +layout: default +design_doc: true +revision: 1 +status: released (6.0) +--- + +This document describes design details for the +PR-1006 requirements. + +XAPI and XenAPI +=============== + +Creating a Bond +--------------- + +### Current Behaviour on Bond creation + +Steps for a user to create a bond: + +1. Shutdown all VMs with VIFs using the interfaces that will be bonded, + in order to unplug those VIFs. +2. Create a Network to be used by the bond: `Network.create` +3. Call `Bond.create` with a ref to this Network, a list of refs of + slave PIFs, and a MAC address to use. +4. Call `PIF.reconfigure_ip` to configure the bond master. +5. Call `Host.management_reconfigure` if one of the slaves is the + management interface. This command will call `interface-reconfigure` + to bring up the master and bring down the slave PIFs, thereby + activating the bond. Otherwise, call `PIF.plug` to activate the + bond. + +`Bond.create` XenAPI call: + +1. Remove duplicates in the list of slaves. +2. Validate the following: + - Slaves must not be in a bond already. + - Slaves must not be VLAN masters. + - Slaves must be on the same host. + - Network does not already have a PIF on the same host as the + slaves. + - The given MAC is valid. + +3. Create the master PIF object. + - The device name of this PIF is `bond`*x*, with *x* the smallest + unused non-negative integer. + - The MAC of the first-named slave is used if no MAC was + specified. + +4. Create the Bond object, specifying a reference to the master. The + value of the `PIF.master_of` field on the master is dynamically + computed on request. +5. Set the `PIF.bond_slave_of` fields of the slaves. The value of the + `Bond.slaves` field is dynamically computed on request. + +### New Behaviour on Bond creation + +Steps for a user to create a bond: + +1. Create a Network to be used by the bond: `Network.create` +2. Call `Bond.create` with a ref to this Network, a list of refs of + slave PIFs, and a MAC address to use.\ + The new bond will automatically be plugged if one of the slaves was + plugged. + +In the following, for a host *h*, a *VIF-to-move* is a VIF associated +with a VM that is either + +- running, suspended or paused on *h*, OR +- halted, and *h* is the only host that the VM can be started on. + +The `Bond.create` XenAPI call is updated to do the following: + +1. Remove duplicates in the list of slaves. +2. Validate the following, and raise an exception if any of these check + fails: + - Slaves must not be in a bond already. + - Slaves must not be VLAN masters. + - Slaves must not be Tunnel access PIFs. + - Slaves must be on the same host. + - Network does not already have a PIF on the same host as the + slaves. + - The given MAC is valid. + +3. Try unplugging all currently attached VIFs of the set of VIFs that + need to be moved. Roll back and raise an exception of one of the + VIFs cannot be unplugged (e.g. due to the absence of PV drivers in + the VM). +4. Determine the *primary slave*: the management PIF (if among the + slaves), or the first slave with IP configuration. +5. Create the master PIF object. + - The device name of this PIF is `bond`*x*, with *x* the smallest + unused non-negative integer. + - The MAC of the primary slave is used if no MAC was specified. + - Include the IP configuration of the primary slave. + - If any of the slaves has `PIF.disallow_unplug = true`, this will + be copied to the master. + +6. Create the Bond object, specifying a reference to the master. The + value of the `PIF.master_of` field on the master is dynamically + computed on request. Also a reference to the primary slave is + written to `Bond.primary_slave` on the new Bond object. +7. Set the `PIF.bond_slave_of` fields of the slaves. The value of the + `Bond.slaves` field is dynamically computed on request. +8. Move VLANs, plus the VIFs-to-move on them, to the master. + - If all VLANs on the slaves have different tags, all VLANs will + be moved to the bond master, while the same Network is used. The + network effectively moves up to the bond and therefore no VIFs + need to be moved. + - If multiple VLANs on different slaves have the same tag, they + necessarily have different Networks as well. Only one VLAN with + this tag is created on the bond master. All VIFs-to-move on the + remaining VLAN networks are moved to the Network that was moved + up. + +9. Move Tunnels to the master. The tunnel Networks move up with the + tunnels. As tunnel keys are different for all tunnel networks, there + are no complications as in the VLAN case. +10. Move VIFs-to-move on the slaves to the master. +11. If one of the slaves is the current management interface, move + management to the master; the master will automatically be plugged. + If none of the slaves is the management interface, plug the master + if any of the slaves was plugged. In both cases, the slaves will + automatically be unplugged. +12. On all slaves, reset the IP configuration and set `disallow_unplug` + to false. + +*Note: "moving" a VIF, VLAN or tunnel means "re-creating somewhere else, +and destroying the old one".* + +Destroying a Bond +----------------- + +### Current Behaviour on Bond destruction + +Steps for a user to destroy a bond: + +1. If the management interface is on the bond, move it to another PIF + using `PIF.reconfigure_ip` and `Host.management_reconfigure`. + Otherwise, no `PIF.unplug` needs to be called on the bond master, as + `Bond.destroy` does this automatically. +2. Call `Bond.destroy` with a ref to the Bond object. +3. If desired, bring up the former slave PIFs by calls to `PIF.plug` + (this is does not happen automatically). + +`Bond.destroy` XenAPI call: + +1. Validate the following constraints: + - No VLANs are attached to the bond master. + - The bond master is not the management PIF. + +2. Bring down the master PIF and clean up the underlying network + devices. +3. Remove the Bond and master PIF objects. + +### New Behaviour on Bond destruction + +Steps for a user to destroy a bond: + +1. Call `Bond.destroy` with a ref to the Bond object. +2. If desired, move VIFs/VLANs/tunnels/management from (former) primary + slave to other PIFs. + +`Bond.destroy` XenAPI call is updated to do the following: + +1. Try unplugging all currently attached VIFs of the set of VIFs that + need to be moved. Roll back and raise an exception of one of the + VIFs cannot be unplugged (e.g. due to the absence of PV drivers in + the VM). +2. Copy the IP configuration of the master to the primary slave. +3. Move VLANs, with their Networks, to the primary slave. +4. Move Tunnels, with their Networks, to the primary slave. +5. Move VIFs-to-move on the master to the primary slave. +6. If the master is the current management interface, move management + to the primary slave. The primary slave will automatically be + plugged. +7. If the master was plugged, plug the primary slave. This will + automatically clean up the underlying devices of the bond. +8. If the master has `PIF.disallow_unplug = true`, this will be copied + to the primary slave. +9. Remove the Bond and master PIF objects. + +Using Bond Slaves +----------------- + +### Current Behaviour for Bond Slaves + +- It possible to plug any existing PIF, even bond slaves. Any other + PIFs that cannot be attached at the same time as the PIF that is + being plugged, are automatically unplugged. +- Similarly, it is possible to make a bond slave the management + interface. Any other PIFs that cannot be attached at the same time + as the PIF that is being plugged, are automatically unplugged. +- It is possible to have a VIF on a Network associated with a bond + slave. When the VIF's VM is started, or the VIF is hot-plugged, the + PIF is relies on is automatically plugged, and any other PIFs that + cannot be attached at the same time as this PIF are automatically + unplugged. +- It is possible to have a VLAN on a bond slave, though the bond + (master) and the VLAN may not be simultaneously attached. This is + not currently enforced (which may be considered a bug). + +### New behaviour for Bond Slaves + +- It is no longer possible to plug a bond slave. The exception + CANNOT\_PLUG\_BOND\_SLAVE is raised when trying to do so. +- It is no longer possible to make a bond slave the management + interface. The exception CANNOT\_PLUG\_BOND\_SLAVE is raised when + trying to do so. +- It is still possible to have a VIF on the Network of a bond slave. + However, it is not possible to start such a VIF's VM on a host, if + this would need a bond slave to be plugged. Trying this will result + in a CANNOT\_PLUG\_BOND\_SLAVE exception. Likewise, it is not + possible to hot-plug such a VIF. +- It is no longer possible to place a VLAN on a bond slave. The + exception CANNOT\_ADD\_VLAN\_TO\_BOND\_SLAVE is raised when trying + to do so. +- It is no longer possible to place a tunnel on a bond slave. The + exception CANNOT\_ADD\_TUNNEL\_TO\_BOND\_SLAVE is raised when trying + to do so. + +Actions on Start-up +------------------- + +### Current Behaviour on Start-up + +When a pool slave starts up, bonds and VLANs on the pool master are +replicated on the slave: + +- Create all VLANs that the master has, but the slave has not. VLANs + are identified by their tag, the device name of the slave PIF, and + the Networks of the master and slave PIFs. +- Create all bonds that the master has, but the slave has not. If the + interfaces needed for the bond are not all available on the slave, a + partial bond is created. If some of these interface are already + bonded on the slave, this bond is destroyed first. + +### New Behaviour on Start-up + +- The current VLAN/tunnel/bond recreation code is retained, as it uses + the new Bond.create and Bond.destroy functions, and therefore does + what it needs to do. +- Before VLAN/tunnel/bond recreation, any violations of the rules + defined in R2 are rectified, by moving VIFs, VLANs, tunnels or + management up to bonds. + +CLI +=== + +The behaviour of the `xe` CLI commands `bond-create`, `bond-destroy`, +`pif-plug`, and `host-management-reconfigure` is changed to match their +associated XenAPI calls. + +XenCenter +========= + +XenCenter already automatically moves the management interface when a +bond is created or destroyed. This is no longer necessary, as the +`Bond.create/destroy` calls already do this. XenCenter only needs to +copy any `PIF.other_config` keys that is needs between primary slave and +bond master. + +Manual Tests +============ + +- Create a bond of two interfaces... + - without VIFs/VLANs/management on them; + - with management on one of them; + - with a VLAN on one of them; + - with two VLANs on two different interfaces, having the same VLAN + tag; + - with a VIF associated with a halted VM on one of them; + - with a VIF associated with a running VM (with and without PV + drivers) on one of them. +- Destroy a bond of two interfaces... + - without VIFs/VLANs/management on it; + - with management on it; + - with a VLAN on it; + - with a VIF associated with a halted VM on it; + - with a VIF associated with a running VM (with and without PV + drivers) on it. +- In a pool of two hosts, having VIFs/VLANs/management on the + interfaces of the pool slave, create a bond on the pool master, and + restart XAPI on the slave. +- Restart XAPI on a host with a networking configuration that has + become illegal due to these requirements. + diff --git a/doc/content/design/coverage/coverage-screenshot.png b/doc/content/design/coverage/coverage-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f14d8070e3fb0b9a263018beb5b49cf9ee192dc9 GIT binary patch literal 111622 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

m;4K>-I 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 JsyHYm?|)V9CZ+%Y literal 0 HcmV?d00001 diff --git a/doc/content/design/coverage/index.md b/doc/content/design/coverage/index.md new file mode 100644 index 00000000000..3b3f6ec3ec7 --- /dev/null +++ b/doc/content/design/coverage/index.md @@ -0,0 +1,267 @@ +--- +layout: default +title: Code Coverage Profiling +design_doc: true +status: proposed +revision: 2 +--- + +We would like to add optional coverage profiling to existing [OCaml] +projects in the context of [XenServer] and [XenAPI]. This article +presents how we do it. + +Binaries instrumented for coverage profiling in the XenServer project +need to run in an environment where several services act together as +they provide operating-system-level services. This makes it a little +harder than profiling code that can be profiled and executed in +isolation. + +## TL;DR + +To build binaries with coverage profiling, do: + + ./configure --enable-coverage + make + +Binaries will log coverage data to `/tmp/bisect*.out` from which a +coverage report can be generated in `coverage/`: + + bisect-ppx-report -I _build -html coverage /tmp/bisect*.out + + +## Profiling Framework Bisect-PPX + +The open-source [BisectPPX] instrumentation framework uses extension +points (PPX) in the [OCaml] compiler to instrument code during +compilation. Instrumented code for a binary is then compiled as usual +and logs during execution data to in-memory data structures. Before an +instrumented binary terminates, it writes the logged data to a file. +This data can then be analysed with the `bisect-ppx-report` tool, to +produce a summary of annotated code that highlights what part of a +codebase was executed. + +[BisectPPX] has several desirable properties: + +* a robust code base that is well tested +* it is easy to integrate into the compilation pipeline (see below) +* is specific to the [OCaml] language; an expression-oriented language + like OCaml doesn't fit the traditional statement coverage well +* it is actively maintained +* is generates useful reports for interactive and non-interactive use + that help to improve code coverage + +![Coverage Analysis](./coverage-screenshot.png) + +Red parts indicate code that wasn't executed whereas green parts were. +Hovering over a dark green spot reveals how often that point was +executed. + +The individual steps of instrumenting code with [BisectPPX] are greatly +abstracted by OCamlfind (OCaml's library manager) and OCamlbuild +(OCaml's compilation manager): + + # write code + vim example.ml + + # build it with instrumentation from bisect_ppx + ocamlbuild -use-ocamlfind -pkg bisect_ppx -pkg unix example.native + + # execute it - generates files ./bisect*.out + ./example.native + + # generate report + bisect-ppx-report -I _build -html coverage bisect000* + + # view coverage/index.html + + Summary: + - 'binding' points: 2/2 (100.00%) + - 'sequence' points: 10/10 (100.00%) + - 'match/function' points: 5/8 (62.50%) + - total: 17/20 (85.00%) + +The fourth step generates a HTML report in `coverage/`. All it takes is +to declare to [OCamlbuild] that a module depends on `bisect_ppx` and it +will be instrumented during compilation. Behind the scenes `ocamlfind` +makes sure that the compiler uses a preprocessing step that instruments +the code. + +## Signal Handling + +During execution the code instrumentation leads to the collection of +data. This code registers a function with `at_exit` that writes the data +to `bisect*.out` when `exit` is called. A binary can terminate without +calling `exit` and in that case the file would not be written. It is +therefore important to make sure that `exit` is called. If this does not +happen naturally, for example in the context of a daemon that is +terminated by receiving the `TERM` signal, a signal handler must be +installed: + + let stop signal = + printf "caught signal %d\n" signal; + exit 0 + + Sys.set_signal Sys.sigterm (Sys.Signal_handle stop) + +## Dumping coverage information at runtime + +By default coverage data can only be dumped at exit, which is inconvenient if you have a test-suite +that needs to reuse a long running daemon, and starting/stopping it each time is not feasible. + +In such cases we need an API to dump coverage at runtime, which *is* provided by `bisect_ppx >= 1.3.0`. +However each daemon will need to set up a way to listen to an event that triggers this coverage dump, +furthermore it is desirable to make runtime coverage dumping compiled in conditionally to be absolutely sure +that production builds do *not* use coverage preprocessed code. + +Hence instead of duplicating all this build logic in each daemon (`xapi`, `xenopsd`, etc.) provide this +functionality in a common library `xapi-idl` that: + + * logs a message on startup so we know it is active + * sets BISECT_FILE environment variable to dump coverage in the appropriate place + * listens on `org.xen.xapi.coverage.` message queue for runtime coverage dump commands: + * sending `dump ` will cause runtime coverage to be dumped to a file + named `bisect--..out` + * sending `reset` will cause the runtime coverage counters to be reset + +Daemons that use `Xcp_service.configure2` (e.g. `xenopsd`) will benefit from this runtime trigger automatically, +provided they are themselves preprocessed with `bisect_ppx`. + +Since we are interested in collecting coverage data for system-wide test-suite runs we need a way to trigger +dumping of coverage data centrally, and a good candidate for that is `xapi` as the top-level daemon. + +It will call `Xcp_coverage.dispatcher_init ()`, which listens on `org.xen.xapi.coverage.dispatch` and +dispatches the coverage dump command to all message queues under `org.xen.xapi.coverage.*` except itself. + +On production, and regular builds all of this is a no-op, ensured by using separate `lib/coverage/disabled.ml` and `lib/coverage/enabled.ml` +files which implement the same interface, and choosing which one to use at build time. + + +## Where Data is Written + +By default, [BisectPPX] writes data in a binary's current working +directory as `bisectXXXX.out`. It doesn't overwrite existing files and +files from several runs can be combined during analysis. However, this +name and the location can be inconvenient when multiple programs share a +directory. + +[BisectPPX]'s default can be overridden with the `BISECT_FILE` +environment variable. This can happen on the command line: + + BISECT_FILE=/tmp/example ./example.native + +In the context of XenServer we could do this in startup scripts. +However, we added a bit of code + + val Coverage.init: string -> unit + +that sets the environment variable from inside the program. The files +are written to a temporary directory (respecting `$TMP` or using `/tmp`) +and uses the `string`-typed argument to include it in the name. To be +effective, this function must be called before the programs exits. For +clarity it is called at the begin of program execution. + +## Instrumenting an Oasis Project + +While instrumentation is easy on the level of a small file or project it +is challenging in a bigger project. We decided to focus on projects that +are build with the [Oasis] build and packaging manager. These have a +well-defined structure and compilation process that is controlled by a +central `_oasis` file. This file describes for each library and binary +its dependencies at a package level. From this, [Oasis] generates a +`configure` script and compilation rules for the [OCamlbuild] system. +[Oasis] is designed that the generated files can be shipped without +requiring [Oasis] itself being available. + +Goals for instrumentation are: + +* what files are instrumented should be obvious and easy to manage +* instrumentation must be optional, yet easy to activate +* avoid methods that require to keep several files in sync like multiple + `_oasis` files +* avoid separate Git branches for instrumented and non-instrumented + code + +In the ideal case, we could introduce a configuration switch +`./configure --enable-coverage` that would prepare compilation for +coverage instrumentation. While [Oasis] supports the creation of such +switches, they cannot be used to control build dependencies like +compiling a file with or without package `bisec_ppx`. We have chosen a +different method: + +A `Makefile` target `coverage` augments the `_tags` file to include the +rules in file `_tags.coverage` that cause files to be instrumented: + + make coverage # prepare + make # build + +leads to the execution of this code during preparation: + + coverage: _tags _tags.coverage + test ! -f _tags.orig && mv _tags _tags.orig || true + cat _tags.coverage _tags.orig > _tags + +The file `_tags.coverage` contains two simple [OCamlbuild] rules that +could be tweaked to instrument only some files: + + <**/*.ml{,i,y}>: pkg_bisect_ppx + <**/*.native>: pkg_bisect_ppx + +When `make coverage` is not called, these rules are not active and +hence, code is not instrumented for coverage. We believe that this +solution to control instrumentation meets the goals from above. In +particular, what files are instrumented and when is controlled by very +few lines of declarative code that lives in the main repository of a +project. + +## Project Layout + +The crucial files in an [Oasis]-controlled project that is set up for +coverage analysis are: + + ./_oasis - make "profiling" a build depdency + ./_tags.coverage - what files get instrumented + ./profiling/coverage.ml - support file, sets env var + ./Makefile - target 'coverage' + +The `_oasis` file bundles the files under `profiling/` into an internal +library which executables then depend on: + + # Support files for profiling + Library profiling + CompiledObject: best + Path: profiling + Install: false + Findlibname: profiling + Modules: Coverage + BuildDepends: + + Executable set_domain_uuid + CompiledObject: best + Path: tools + ByteOpt: -warn-error +a-3 + NativeOpt: -warn-error +a-3 + MainIs: set_domain_uuid.ml + Install: false + BuildDepends: + xenctrl, + uuidm, + cmdliner, + profiling # <-- here + +The `Makefile` target `coverage` primes the project for a profiling build: + + # make coverage - prepares for building with coverage analysis + + coverage: _tags _tags.coverage + test ! -f _tags.orig && mv _tags _tags.orig || true + cat _tags.coverage _tags.orig > _tags + + +[OCamlbuild]: https://github.com/ocaml/ocamlbuild/blob/master/manual/manual.adoc +[BisectPPX]: https://github.com/aantron/bisect_ppx +[OCaml]: http://ocaml.org +[XenServer]: https://github.com/xenserver +[XenAPI]: https://github.com/xapi-project +[Oasis]: http://oasis.forge.ocamlcore.org + + diff --git a/doc/content/design/cpu-levelling-v2.md b/doc/content/design/cpu-levelling-v2.md new file mode 100644 index 00000000000..2192c1665a3 --- /dev/null +++ b/doc/content/design/cpu-levelling-v2.md @@ -0,0 +1,202 @@ +--- +title: CPU feature levelling 2.0 +layout: default +design_doc: true +status: released (7.0) +revision: 7 +revision_history: +- revision_number: 1 + description: Initial version +- revision_number: 2 + description: Add details about VM migration and import +- revision_number: 3 + description: Included and excluded use cases +- revision_number: 4 + description: Rolling Pool Upgrade use cases +- revision_number: 5 + description: Lots of changes to simplify the design +- revision_number: 6 + description: Use case refresh based on simplified design +- revision_number: 7 + description: RPU refresh based on simplified design +--- + +Executive Summary +================= + +The old XS 5.6-style Heterogeneous Pool feature that is based around hardware-level CPUID masking will be replaced by a safer and more flexible software-based levelling mechanism. + +History +======= + +- Original XS 5.6 design: [heterogeneous-pools](../heterogeneous-pools) +- Changes made in XS 5.6 FP1 for the DR feature (added CPUID checks upon migration) +- XS 6.1: migration checks extended for cross-pool scenario + +High-level Interfaces and Behaviour +=================================== + +A VM can only be migrated safely from one host to another if both hosts offer the set of CPU features which the VM expects. If this is not the case, CPU features may appear or disappear as the VM is migrated, causing it to crash. The purpose of feature levelling is to hide features which the hosts do not have in common from the VM, so that it does not see any change in CPU capabilities when it is migrated. + +Most pools start off with homogenous hardware, but over time it may become impossible to source new hosts with the same specifications as the ones already in the pool. The main use of feature levelling is to allow such newer, more capable hosts to be added to an existing pool while preserving the ability to migrate existing VMs to any host in the pool. + +Principles for Migration +------------------------ + +The CPU levelling feature aims to both: + +1. Make VM migrations _safe_ by ensuring that a VM will see the same CPU features before and after a migration. +2. Make VMs as _mobile_ as possible, so that it can be freely migrated around in a XenServer pool. + +To make migrations safe: + +* A migration request will be blocked if the destination host does not offer the some of the CPU features that the VM currently sees. +* Any additional CPU features that the destination host is able to offer will be hidden from the VM. + +_Note:_ Due to the limitations of the old Heterogeneous Pools feature, we are not able to guarantee the safety of VMs that are migrated to a Levelling-v2 host from an older host, during a rolling pool upgrade. This is because such VMs may be using CPU features that were not captured in the old feature sets, of which we are therefore unaware. However, migrations between the same two hosts, but before the upgrade, may have already been unsafe. The promise is that we will not make migrations _more_ unsafe during a rolling pool upgrade. + +To make VMs mobile: + +* A VM that is started in a XenServer pool will be able to see only CPU features that are common to all hosts in the pool. The set of common CPU features is referred to in this document as the _pool CPU feature level_, or simply the _pool level_. + +Use Cases for Pools +------------------- + +1. A user wants to add a new host to an existing XenServer pool. The new host has all the features of the existing hosts, plus extra features which the existing hosts do not. The new host will be allowed to join the pool, but its extra features will be hidden from VMs that are started on the host or migrated to it. The join does not require any host reboots. + +2. A user wants to add a new host to an existing XenServer pool. The new host does not have all the features of the existing ones. XenCenter warns the user that adding the host to the pool is possible, but it would lower the pool's CPU feature level. The user accepts this and continues the join. The join does not require any host reboots. VMs that are started anywhere on the pool, from now on, will only see the features of the new host (the lowest common denominator), such that they are migratable to any host in the pool, including the new one. VMs that were running before the pool join will not be migratable to the new host, because these VMs may be using features that the new host does not have. However, after a reboot, such VMs will be fully mobile. + +3. A user wants to add a new host to an existing XenServer pool. The new host does not have all the features of the existing ones, and at the same time, it has certain features that the pool does not have (the feature sets overlap). This is essentially a combination of the two use cases above, where the pool's CPU feature level will be downgraded to the intersection of the feature sets of the pool and the new host. The join does not require any host reboots. + +4. A user wants to upgrade or repair the hardware of a host in an existing XenServer pool. After upgrade the host has all the features it used to have, plus extra features which other hosts in the pool do not have. The extra features are masked out and the host resumes its place in the pool when it is booted up again. + +5. A user wants to upgrade or repair the hardware of a host in an existing XenServer pool. After upgrade the host has fewer features than it used to have. When the host is booted up again, the pool CPU's feature level will be automatically lowered, and the user will be alerted of this fact (through the usual alerting mechanism). + +6. A user wants to remove a host from an existing XenServer pool. The host will be removed as normal after any VMs on it have been migrated away. The feature set offered by the pool will be automatically re-levelled upwards in case the host which was removed was the least capable in the pool, and additional features common to the remaining hosts will be unmasked. + + +Rolling Pool Upgrade +-------------------- + +* A VM which was running on the pool before the upgrade is expected to continue to run afterwards. However, when the VM is migrated to an upgraded host, some of the CPU features it had been using might disappear, either because they are not offered by the host or because the new feature-levelling mechanism hides them. To have the best chance for such a VM to successfully migrate (see the note under "Principles for Migration"), it will be given a temporary VM-level feature set providing all of the destination's CPU features that were unknown to XenServer before the upgrade. When the VM is rebooted it will inherit the pool-level feature set. + +* A VM which is started during the upgrade will be given the current pool-level feature set. The pool-level feature set may drop after the VM is started, as more hosts are upgraded and re-join the pool, however the VM is guaranteed to be able to migrate to any host which has already been upgraded. If the VM is started on the master, there is a risk that it may only be able to run on that host. + +* To allow the VMs with grandfathered-in flags to be migrated around in the pool, the intra pool VM migration pre-checks will compare the VM's feature flags to the target host's flags, not the pool flags. This will maximise the chance that a VM can be migrated somewhere in a heterogeneous pool, particularly in the case where only a few hosts in the pool do not have features which the VMs require. + +* To allow cross-pool migration, including to pool of a higher XenServer version, we will still check the VM's requirements against the *pool-level* features of the target pool. This is to avoid the possibility that we migrate a VM to an 'island' in the other pool, from which it cannot be migrated any further. + + +XenAPI Changes +-------------- + +### Fields + +* `host.cpu_info` is a field of type `(string -> string) map` that contains information about the CPUs in a host. It contains the following keys: `cpu_count`, `socket_count`, `vendor`, `speed`, `modelname`, `family`, `model`, `stepping`, `flags`, `features`, `features_after_reboot`, `physical_features` and `maskable`. + * The following keys are specific to hardware-based CPU masking and will be removed: `features_after_reboot`, `physical_features` and `maskable`. + * The `features` key will continue to hold the current CPU features that the host is able to use. In practise, these features will be available to Xen itself and dom0; guests may only see a subset. The current format is a string of four 32-bit words represented as four groups of 8 hexadecimal digits, separated by dashes. This will change to an arbitrary number of 32-bit words. Each bit at a particular position (starting from the left) still refers to a distinct CPU feature (`1`: feature is present; `0`: feature is absent), and feature strings may be compared between hosts. The old format simply becomes a special (4 word) case of the new format, and bits in the same position may be compared between old and new feature strings. + * The new key `features_pv` will be added, representing the subset of `features` that the host is able to offer to a PV guest. + * The new key `features_hvm` will be added, representing the subset of `features` that the host is able to offer to an HVM guest. +* A new field `pool.cpu_info` of type `(string -> string) map` (read only) will be added. It will contain: + * `vendor`: The common CPU vendor across all hosts in the pool. + * `features_pv`: The intersection of `features_pv` across all hosts in the pool, representing the feature set that a PV guest will see when started on the pool. + * `features_hvm`: The intersection of `features_hvm` across all hosts in the pool, representing the feature set that an HVM guest will see when started on the pool. + * `cpu_count`: the total number of CPU cores in the pool. + * `socket_count`: the total number of CPU sockets in the pool. +* The `pool.other_config:cpuid_feature_mask` override key will no longer have any effect on pool join or VM migration. +* The field `VM.last_boot_CPU_flags` will be updated to the new format (see `host.cpu_info:features`). It will still contain the feature set that the VM was started with as well as the vendor (under the `features` and `vendor` keys respectively). + +### Messages + +* `pool.join` currently requires that the CPU vendor and feature set (according to `host.cpu_info:vendor` and `host.cpu_info:features`) of the joining host are equal to those of the pool master. This requirement will be loosened to mandate only equality in CPU vendor: + * The join will be allowed if `host.cpu_info:vendor` equals `pool.cpu_info:vendor`. + * This means that xapi will additionally allow hosts that have a _more_ extensive feature set than the pool (as long as the CPU vendor is common). Such hosts are transparently down-levelled to the pool level (without needing reboots). + * This further means that xapi will additionally allow hosts that have a _less_ extensive feature set than the pool (as long as the CPU vendor is common). In this case, the pool is transparently down-levelled to the new host's level (without needing reboots). Note that this does not affect any running VMs in any way; the mobility of running VMs will not be restricted, which can still migrate to any host they could migrate to before. It does mean that those running VMs will not be migratable to the new host. + * The current error raised in case of a CPU mismatch is `POOL_HOSTS_NOT_HOMOGENEOUS` with `reason` argument `"CPUs differ"`. This will remain the error that is raised if the pool join fails due to incompatible CPU vendors. + * The `pool.other_config:cpuid_feature_mask` override key will no longer have any effect. +* `host.set_cpu_features` and `host.reset_cpu_features` will be removed: it is no longer to use the old method of CPU feature masking (CPU feature sets are controlled automatically by xapi). Calls will fail with `MESSAGE_REMOVED`. +* VM lifecycle operations will be updated internally to use the new feature fields, to ensure that: + * Newly started VMs will be given CPU features according to the pool level for maximal mobility. + * For safety, running VMs will maintain their feature set across migrations and suspend/resume cycles. CPU features will transparently be hidden from VMs. + * Furthermore, migrate and resume will only be allowed in case the target host's CPUs are capable enough, i.e. `host.cpu_info:vendor` = `VM.last_boot_CPU_flags:vendor` and `host.cpu_info:features_{pv,hvm}` ⊇ `VM.last_boot_CPU_flags:features`. A `VM_INCOMPATIBLE_WITH_THIS_HOST` error will be returned otherwise (as happens today). + * For cross pool migrations, to ensure maximal mobility in the target pool, a stricter condition will apply: the VM must satisfy the pool CPU level rather than just the target host's level: `pool.cpu_info:vendor` = `VM.last_boot_CPU_flags:vendor` and `pool.cpu_info:features_{pv,hvm}` ⊇ `VM.last_boot_CPU_flags:features` + + +CLI Changes +----------- + +The following changes to the `xe` CLI will be made: + +* `xe host-cpu-info` (as well as `xe host-param-list` and friends) will return the fields of `host.cpu_info` as described above. +* `xe host-set-cpu-features` and `xe host-reset-cpu-features` will be removed. +* `xe host-get-cpu-features` will still return the value of `host.cpu_info:features` for a given host. + +Low-level implementation +======================== + +Xenctrl +------- + +The old `xc_get_boot_cpufeatures` hypercall will be removed, and replaced by two new functions, which are available to xenopsd through the Xenctrl module: + + external get_levelling_caps : handle -> int64 = "stub_xc_get_levelling_caps" + + type featureset_index = Featureset_host | Featureset_pv | Featureset_hvm + external get_featureset : handle -> featureset_index -> int64 array = "stub_xc_get_featureset" + +In particular, the `get_featureset` function will be used by xapi/xenopsd to ask Xen which are the widest sets of CPU features that it can offer to a VM (PV or HVM). I don't think there is a use for `get_levelling_caps` yet. + +Xenopsd +------- + +* Update the type `Host.cpu_info`, which contains all the fields that need to go into the `host.cpu_info` field in the xapi DB. The type already exists but is unused. Add the function `HOST.get_cpu_info` to obtain an instance of the type. Some code from xapi and the cpuid.ml from xen-api-libs can be reused. +* Add a platform key `featureset` (`Vm.t.platformdata`), which xenopsd will write to xenstore along with the other platform keys (no code change needed in xenopsd). Xenguest will pick this up when a domain is created, and will apply the CPUID policy to the domain. This has the effect of masking out features that the host may have, but which have a `0` in the feature set bitmap. +* Review current cpuid-related functions in `xc/domain.ml`. + +Xapi +---- + +### Xapi startup + +* Update `Create_misc.create_host_cpu` function to use the new xenopsd call. +* If the host features fall below pool level, e.g. due to a change in hardware: down-level the pool by updating `pool.cpu_info.features_{pv,hvm}`. Newly started VMs will inherit the new level; already running VMs will not be affected, but will not be able to migrate to this host. +* To notify the admin of this event, an API alert (message) will be set: `pool_cpu_features_downgraded`. + +### VM start + +- Inherit feature set from pool (`pool.cpu_info.features_{pv,hvm}`) and set `VM.last_boot_CPU_flags` (`cpuid_helpers.ml`). +- The domain will be started with this CPU feature set enabled, by writing the feature set string to `platformdata` (see above). + +### VM migrate and resume + +- There are already CPU compatiblity checks on migration, both in-pool and cross-pool, as well as resume. Xapi compares `VM.last_boot_CPU_flags` of the VM to-migrate with `host.cpu_info` of the receiving host. Migration is only allowed if the CPU vendors and the same, and `host.cpu_info:features` ⊇ `VM.last_boot_CPU_flags:features`. The check can be overridden by setting the `force` argument to `true`. +- For in-pool migrations, these checks will be updated to use the appropriate `features_pv` or `features_hvm` field. +- For cross-pool migrations. These checks will be updated to use `pool.cpu_info` (`features_pv` or `features_hvm` depending on how the VM was booted) rather than `host.cpu_info`. +- If the above checks pass, then the `VM.last_boot_CPU_flags` will be maintained, and the new domain will be started with the same CPU feature set enabled, by writing the feature set string to `platformdata` (see above). +- In case the VM is migrated to a host with a higher xapi software version (e.g. a migration from a host that does not have CPU levelling v2), the feature string may be longer. This may happen during a rolling pool upgrade or a cross-pool migration, or when a suspended VM is resume after an upgrade. In this case, the following safety rules apply: + - Only the existing (shorter) feature string will be used to determine whether the migration will be allowed. This is the best we can do, because we are unaware of the state of the extended feature set on the older host. + - The existing feature set in `VM.last_boot_CPU_flags` will be extended with the extra bits in `host.cpu_info:features_{pv,hvm}`, i.e. the widest feature set that can possibly be granted to the VM (just in case the VM was using any of these features before the migration). + - Strictly speaking, a migration of a VM from host A to B that was allowed before B was upgraded, may no longer be allowed after the upgrade, due to stricter feature sets in the new implementation (from the `xc_get_featureset` hypercall). However, the CPU features that are switched off by the new implementation are features that a VM would not have been able to actually use. We therefore need a don't-care feature set (similar to the old `pool.other_config:cpuid_feature_mask` key) with bits that we may ignore in migration checks, and switch off after the migration. This will be a xapi config file option. + - XXX: Can we actually block a cross-pool migration at the receiver end?? + +### VM import + +The `VM.last_boot_CPU_flags` field must be upgraded to the new format (only really needed for VMs that were suspended while exported; `preserve_power_state=true`), as described above. + +### Pool join + +Update pool join checks according to the rules above (see `pool.join`), i.e. remove the CPU features constraints. + +### Upgrade + +* The pool level (`pool.cpu_info`) will be initialised when the pool master upgrades, and automatically adjusted if needed (downwards) when slaves are upgraded, by each upgraded host's started sequence (as above under "Xapi startup"). +* The `VM.last_boot_CPU_flags` fields of running and suspended VMs will be "upgraded" to the new format on demand, when a VM is migrated to or resume on an upgraded host, as described above. + + +XenCenter integration +--------------------- + +- Don't explicitly down-level upon join anymore +- Become aware of new pool join rule +- Update Rolling Pool Upgrade + diff --git a/doc/content/design/distributed-database/architecture.png b/doc/content/design/distributed-database/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..5756472b025fef583f2160d52228a2ae0fd2280f GIT binary patch literal 89659 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

xNpA^--Q}D38xEqb5Tb!?^n`Hb`2v-!c&= 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 R@&A3Yy0VT^t-{;r{{y>I;*kIV literal 0 HcmV?d00001 diff --git a/doc/content/design/distributed-database/index.md b/doc/content/design/distributed-database/index.md new file mode 100644 index 00000000000..b56d043d9e8 --- /dev/null +++ b/doc/content/design/distributed-database/index.md @@ -0,0 +1,181 @@ +--- +title: Distributed database +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +All hosts in a pool use the shared database by sending queries to +the pool master. This creates + +- a performance bottleneck as the pool size increases +- a reliability problem when the master fails. + +The reliability problem can be ameliorated by running with HA enabled, +but this is not always possible. + +Both problems can be addressed by observing that the database objects +correspond to distinct physical objects where eventual consistency is +perfectly ok. For example if host 'A' is running a VM and changes the +VM's name, it doesn't matter if it takes a while before the change shows +up on host 'B'. If host 'B' changes its network configuration then it +doesn't matter how long it takes host 'A' to notice. We would still like +the metadata to be replicated to cope with failure, but we can allow +changes to be committed locally and synchronised later. + +Note the one exception to this pattern: the current SM plugins use database +fields to implement locks. This should be shifted to a special-purpose +lock acquire/release API. + +Using git via Irmin +------------------- + +A git repository is a database of key=value pairs with branching history. +If we placed our host and VM metadata in git then we could `commit` +changes and `pull` and `push` them between replicas. The +[Irmin](https://github.com/mirage/irmin) library provides an easy programming +interface on top of git which we could link with the Xapi database layer. + +Proposed new architecture +------------------------- + +![Pools of one](architecture.png) + +The diagram above shows two hosts: one a master and the other a regular host. +The XenAPI client has sent a request to the wrong host; normally this would +result in a `HOST_IS_SLAVE` error being sent to the client. In the new +world, the host is able to process the request, only contacting the master +if it is necessary to acquire a lock. Starting a VM would require a lock; but +rebooting or migrating an existing VM would not. Assuming the lock can +be acquired, then the operation is executed locally with all state updates +being made to a git topic branch. + +![Topic branches](topic.png) + +Roughly we would have 1 topic branch per +pending XenAPI Task. Once the Task completes successfully, the topic branch +(containing the new VM state) is merged back into master. +Separately each +host will pull and push updates between each other for replication. + +We would avoid merge conflicts by construction; either + +- a host's configuration will always be "owned" by the host and it will be + an error for anyone else to merge updates to it +- the master's locking will guarantee that a VM is running on at most one + host at a time. It will be an error for anyone else to merge updates to it. + +What we gain +------------ + +We will gain the following + +- the master will only be a bottleneck when the number of VM locks gets + really large; +- you will be able to connect XenCenter to hosts without a master and manage + them. Today such hosts are unmanageable. +- the database will have a history and you'll be able to "go back in time" + either for debugging or to recover from mistakes +- bugs caused by concurrent threads (in separate Tasks) confusing each other + will be vanquished. A typical failure mode is: one active thread destroys + an object; a passive thread sees the object and then tries to read it + and gets a database failure instead. Since every thread is operating a + separate Task they will all have their own branch and will be isolated from + each other. + +What we lose +------------ + +We will lose the following + +- the ability to use the Xapi database as a "lock" +- coherence between hosts: there will be no guarantee that an effect seen + by host 'A' will be seen immediately by host 'B'. In particular this means + that clients should send all their commands and `event.from` calls to + the same host (although any host will do) + + +Stuff we need to build +---------------------- + +- A `pull`/`push` replicator: this would have to monitor the list + of hosts in the pool and distribute updates to them in some vaguely + efficient manner. Ideally we would avoid hassling the pool master and + use some more efficient topology: perhaps a tree? + +- A `git diff` to XenAPI event converter: whenever a host `pull`s + updates from another it needs to convert the diff into a set of touched + objects for any `event.from` to read. We could send the changeset hash + as the `event.from` token. + +- Irmin nested views: since Tasks can be nested (and git branches can be + nested) we need to make sure that Irmin views can be nested. + +- We need to go through the xapi code and convert all mixtures of database + access and XenAPI updates into pure database calls. With the previous system + it was better to use a XenAPI to remote large chunks of database effects to + the master than to perform them locally. It will now be better to run them + all locally and merge them at the end. Additionally since a Task will have + a local branch, it won't be possible to see the state on a remote host + without triggering an early merge (which would harm efficiency) + +- We need to create a first-class locking API to use instead of the + `VDI.sm_config` locks. + +Prototype +--------- + +A basic prototype has been created: + +```bash +$ opam pin xen-api-client git://github.com/djs55/xen-api-client#improvements +$ opam pin add xapi-database git://github.com/djs55/xapi-database +$ opam pin add xapi git://github.com/djs55/xen-api#schema-sexp +``` + +The `xapi-database` is clone of the existing Xapi database code +configured to run as a separate process. There is +[code to convert from XML to git](https://github.com/djs55/xapi-database/blob/master/core/db_git.ml#L55) +and +[an implementation of the Xapi remote database API](https://github.com/djs55/xapi-database/blob/master/core/db_git.ml#L186) +which uses the following layout: + +```bash +$ git clone /xapi.db db +Cloning into 'db'... +done. + +$ cd db; ls +xapi + +$ ls xapi +console host_metrics PCI pool SR user VM +host network PIF session tables VBD VM_metrics +host_cpu PBD PIF_metrics SM task VDI + +$ ls xapi/pool +OpaqueRef:39adc911-0c32-9e13-91a8-43a25939110b + +$ ls xapi/pool/OpaqueRef\:39adc911-0c32-9e13-91a8-43a25939110b/ +crash_dump_SR __mtime suspend_image_SR +__ctime name_description uuid +default_SR name_label vswitch_controller +ha_allow_overcommit other_config wlb_enabled +ha_enabled redo_log_enabled wlb_password +ha_host_failures_to_tolerate redo_log_vdi wlb_url +ha_overcommitted ref wlb_username +ha_plan_exists_for _ref wlb_verify_cert +master restrictions + +$ ls xapi/pool/OpaqueRef\:39adc911-0c32-9e13-91a8-43a25939110b/other_config/ +cpuid_feature_mask memory-ratio-hvm memory-ratio-pv + +$ cat xapi/pool/OpaqueRef\:39adc911-0c32-9e13-91a8-43a25939110b/other_config/cpuid_feature_mask +ffffff7f-ffffffff-ffffffff-ffffffff +``` + +Notice how: + +- every object is a directory +- every key/value pair is represented as a file diff --git a/doc/content/design/distributed-database/topic.png b/doc/content/design/distributed-database/topic.png new file mode 100644 index 0000000000000000000000000000000000000000..bcb94f19667c1d42f0d841da2c221243c85a029d GIT binary patch literal 55724 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

M>239yygr*@Q-wav*8 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 +Parameter +Description + + +-m, --master +The IP address of the master. Optional if the host is pool slave, ignored otherwise. + + +--device +Device name of management interface. Optional. If not specified, it is taken from the firstboot data. + + +--mode +IP configuration mode for management interface. Optional. Either dhcp or static (default is dhcp). + + +--ip +IP address for management interface. Required if --mode=static, ignored otherwise. + + +--netmask +Netmask for management interface. Required if --mode=static, ignored otherwise. + + +--gateway +Gateway for management interface. Optional; ignored if --mode=dhcp. + + +--dns +DNS server for management interface. Optional; ignored if --mode=dhcp. + + + +DNS server for management interface. Optional; ignored if `--mode=dhcp`. + +The script takes the following steps after processing the given +parameters: + +1. Inform the user that the host will be restarted, and that any + running VMs should be shut down. Make the user confirm that they + really want to reset the networking by typing 'yes'. +2. Read `/etc/xensource/pool.conf` to determine whether the host is a + pool master or pool slave. +3. If a pool slave, update the IP address in the `pool.conf` file to + the one given in the `-m` parameter, if present. +4. Shut down networking subsystem (`service network stop`). +5. If no management device is specified, take it from + /etc/firstboot.d/data/management.conf. +6. If XAPI is running, stop it. +7. Reconfigure the management interface and associated bridge by + `interface-reconfigure --force`. +8. Update `MANAGEMENT_INTERFACE` and clear `CURRENT_INTERFACES` in + `/etc/xensource-inventory`. +9. Create the file `/tmp/network-reset` to trigger XAPI to complete the + network reset after the reboot. This file should contain the full + configuration details of the management interface as key/value pairs + (format: `=\n`), and looks similar to the firstboot data + files. The file contains at least the keys `DEVICE` and `MODE`, and + `IP`, `NETMASK`, `GATEWAY`, or `DNS` when appropriate. +10. Reboot + +XAPI +---- + +### XenAPI + +A new *hidden* API call: + +- `Host.reset_networking` + - Parameter: host reference `host` + - Calling this function removes all the PIF, Bond, VLAN and tunnel + objects associated with the given host from the master database. + All Network and VIF objects are maintained, as these do not + necessarily belong to a single host. + +### Start-up Sequence + +After reboot, in the XAPI start-up sequence trigged by the presence of +`/tmp/network-reset`: + +1. Read the desired management configuration from `/tmp/network-reset`. +2. Call `Host.reset_networking` with a ref to the localhost. +3. Call `PIF.scan` with a ref to the localhost to recreate the + (physical) PIFs. +4. Call `PIF.reconfigure_ip` to configure the management interface. +5. Call `Host.management_reconfigure`. +6. Delete `/tmp/network-reset`. + +xsconsole +--------- + +Add an "Emergency Network Reset" option under the "Network and +Management Interface" menu. Selecting this option will show some +explanation in the pane on the right-hand side. Pressing \ will +bring up a dialogue to select the interfaces to use as management +interface after the reset. After choosing a device, the dialogue +continues with configuration options like in the "Configure Management +Interface" dialogue. After completing the dialogue, the same steps as +listed for `xe-reset-networking` are executed. + +Notes +----- + +- On a pool slave, the management interface should be the same as on + the master (the same device name, e.g. eth0). +- Resetting the networking configuration on the master should be + ideally be followed by resets of the pool slaves as well, in order + to synchronise their configuration (especially bonds/VLANs/tunnels). + Furthermore, in case the IP address of the master has changed, as a + result of a network reset or `Host.management_reconfigure`, pool + slaves may also use the network reset functionality to reconnect to + the master on its new IP. + diff --git a/doc/content/design/emulated-pci-spec.md b/doc/content/design/emulated-pci-spec.md new file mode 100644 index 00000000000..7a976ede1a2 --- /dev/null +++ b/doc/content/design/emulated-pci-spec.md @@ -0,0 +1,61 @@ +--- +title: Specifying Emulated PCI Devices +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +### Background and goals + +At present (early March 2015) the datamodel defines a VM as having a "platform" string-string map, in which two keys are interpreted as specifying a PCI device which should be emulated for the VM. Those keys are "device_id" and "revision" (with int values represented as decimal strings). + +Limitations: +* Hardcoded defaults are used for the the vendor ID and all other parameters except device_id and revision. +* Only one emulated PCI device can be specified. + +When instructing qemu to emulate PCI devices, qemu accepts twelve parameters for each device. + +Future guest-agent features rely on additional emulated PCI devices. We cannot know in advance the full details of all the devices that will be needed, but we can predict some. + +We need a way to configure VMs such that they will be given additional emulated PCI devices. + +### Design + +In the datamodel, there will be a new type of object for emulated PCI devices. + +Tentative name: "emulated_pci_device" + +Fields to be passed through to qemu are the following, all static read-only, and all ints except devicename: +* devicename (string) +* vendorid +* deviceid +* command +* status +* revision +* classcode +* headertype +* subvendorid +* subsystemid +* interruptline +* interruptpin + +We also need a "built_in" flag: see below. + +Allow creation of these objects through the API (and CLI). + +(It would be nice, but by no means essential, to be able to create one by specifying an existing one as a basis, along with one or more altered fields, e.g. "Make a new one just like that existing one except with interruptpin=9.") + +Create some of these devices to be defined as standard in XenServer, along the same lines as the VM templates. Those ones should have built_in=true. + +Allow destruction of these objects through the API (and CLI), but not if they are in use or if they have built_in=true. + +A VM will have a list of zero or more of these emulated-pci-device objects. (OPEN QUESTION: Should we forbid having more than one of a given device?) + +Provide API (and CLI) commands to add and remove one of these devices from a VM (identifying the VM and device by uuid or other identifier such as name). + +The CLI should allow performing this on multiple VMs in one go, based on a selector or filter for the VMs. We have this concept already in the CLI in commands such as vm-start. + +In the function that adds an emulated PCI device to a VM, we must check if this is the first device to be added, and must refuse if the VM's Virtual Hardware Platform Version is too low. (Or should we just raise the version automatically if needed?) + +When starting a VM, check its list of emulated pci devices and pass the details through to qemu (via xenopsd). diff --git a/doc/content/design/fcoe-nics.md b/doc/content/design/fcoe-nics.md new file mode 100644 index 00000000000..59b2634f609 --- /dev/null +++ b/doc/content/design/fcoe-nics.md @@ -0,0 +1,56 @@ +--- +title: FCoE capable NICs +layout: default +design_doc: true +revision: 3 +status: proposed +design_review: 120 +--- + +It has been possible to identify the NICs of a Host which can support FCoE. +This property can be listed in PIF object under capabilities field. + +Introduction +------------ + +* FCoE supported on a NIC is a hardware property. With the help of dcbtool, we can identify which NIC support FCoE. +* The new field capabilities will be `Set(String)` in PIF object. For FCoE capable NIC will have string "fcoe" in PIF capabilities field. +* `capabilities` field will be ReadOnly, This field cannot be modified by user. + +PIF Object +------- + +New field: + +* Field `PIF.capabilities` will be type `Set(string)`. +* Default value in PIF capabilities will have an empty set. + +Xapi Changes +------- + +* Set the field capabilities "fcoe" depending on output of xcp-networkd call `get_capabilities`. +* Field capabilities "fcoe" can be set during `introduce_internal` on when creating a PIF. +* Field capabilities "fcoe" can be updated during `refresh_all` on xapi startup. +* The above field will be set everytime when xapi-restart. + +XCP-Networkd Changes +------- + +New function: + +* String list `string list get_capabilties (string)` +* Argument: device_name for the PIF. +* This function calls method `capable` exposed by `fcoe_driver.py` as part of dom0. +* It returns string list ["fcoe"] or [] depending on `capable` method output. + +Defaults, Installation and Upgrade +------------------------ + +* Any newly introduced PIF will have its capabilities field as empty set until `fcoe_driver` method `capable` states FCoE is supported on the NIC. +* It includes PIFs obtained after a fresh install of Xenserver, as well as PIFs created using `PIF.introduce` then `PIF.scan`. +* During an upgrade Xapi Restart will call `refresh_all` which then populate the capabilities field as empty set. + +Command Line Interface +---------------------- + +* The `PIF.capabilities` field is exposed through `xe pif-list` and `xe pif-param-list` as usual. diff --git a/doc/content/design/gpu-passthrough.md b/doc/content/design/gpu-passthrough.md new file mode 100644 index 00000000000..db6841cee38 --- /dev/null +++ b/doc/content/design/gpu-passthrough.md @@ -0,0 +1,365 @@ +--- +title: GPU pass-through support +layout: default +design_doc: true +revision: 1 +status: released (6.0) +--- + +This document contains the software design for GPU pass-through. This +code was originally included in the version of Xapi used in XenServer 6.0. + +Overview +-------- + +Rather than modelling GPU pass-through from a PCI perspective, and +having the user manipulate PCI devices directly, we are taking a +higher-level view by introducing a dedicated graphics model. The +graphics model is similar to the networking and storage model, in which +virtual and physical devices are linked through an intermediate +abstraction layer (e.g. the "Network" class in the networking model). + +The basic graphics model is as follows: + +- A host owns a number of physical GPU devices (*pGPUs*), each of + which is available for passing through to a VM. +- A VM may have a virtual GPU device (*vGPU*), which means it expects + to have access to a GPU when it is running. +- Identical pGPUs are grouped across a resource pool in *GPU groups*. + GPU groups are automatically created and maintained by XS. +- A GPU group connects vGPUs to pGPUs in the same way as VIFs are + connected to PIFs by Network objects: for a VM *v* having a vGPU on + GPU group *p* to run on host *h*, host *h* must have a pGPU in GPU + group *p* and pass it through to VM *v*. +- VM start and non-live migration rules are analogous to the network + API and follow the above rules. +- In case a VM that has a vGPU is started, while no pGPU available, an + exception will occur and the VM won't start. As a result, in order + to guarantee that a VM always has access to a pGPU, the number of + vGPUs should not exceed the number of pGPUs in a GPU group. + +Currently, the following restrictions apply: + +- Hotplug is not supported. +- Suspend/resume and checkpointing (memory snapshots) are not + supported. +- Live migration (XenMotion) is not supported. +- No more than one GPU per VM will be supported. +- Only Windows guests will be supported. + +XenAPI Changes +-------------- + +The design introduces a new generic class called *PCI* to capture state +and information about relevant PCI devices in a host. By default, xapi +would not create PCI objects for all PCI devices, but only for the ones +that are managed and configured by xapi; currently only GPU devices. + +The PCI class has no fields specific to the type of the PCI device (e.g. +a graphics card or NIC). Instead, device specific objects will contain a +link to their underlying PCI device's object. + +The new XenAPI classes and changes to existing classes are detailed +below. + +### PCI class + +Fields: + +| Name | Type | Description | +|----------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| uuid | string | Unique identifier/object reference. | +| class_id | string | PCI class ID (hidden field) | +| class_name | string | PCI class name (GPU, NIC, ...) | +| vendor_id | string | Vendor ID (hidden field). | +| vendor_name | string | Vendor name. | +| device_id | string | Device ID (hidden field). | +| device_name | string | Device name. | +| host | host ref | The host that owns the PCI device. | +| pci_id | string | BDF (domain/Bus/Device/Function identifier) of the (physical) PCI function, e.g. "0000:00:1a.1". The format is hhhh:hh:hh.h, where h is a hexadecimal digit. | +| functions | int | Number of (physical + virtual) functions; currently fixed at 1 (hidden field). | +| attached_VMs | VM ref set | List of VMs that have this PCI device "currently attached", i.e. plugged, i.e. passed-through to (hidden field). | +| dependencies | PCI ref set | List of dependent PCI devices: all of these need to be passed-thru to the same VM (co-location). | +| other_config | (string -> string) map | Additional optional configuration (as usual). | + +*Hidden fields* are only for use by xapi internally, and not visible to +XenAPI users. + +Messages: none. + +### PGPU class + +A physical GPU device (pGPU). + +Fields: + +| Name | Type | Description | +|----------------|--------------------------|----------------------------------------------------| +| uuid | string | Unique identifier/object reference. | +| PCI | PCI ref | Link to the underlying PCI device. | +| other_config | (string -> string) map | Additional optional configuration (as usual). | +| host | host ref | The host that owns the GPU. | +| GPU_group | GPU_group ref | GPU group the pGPU is contained in. Can be Null. | + +Messages: none. + +### GPU\_group class + +A group of identical GPUs across hosts. A VM that is associated with a +GPU group can use any of the GPUs in the group. A VM does not need to +install new GPU drivers if moving from one GPU to another one in the +same GPU group. + +Fields: + +| Name | Type | Description | +|--------------------|--------------------------|----------------------------------------------------------------------------------| +| VGPUs | VGPU ref set | List of vGPUs in the group. | +| uuid | string | Unique identifier/object reference. | +| PGPUs | PGPU ref set | List of pGPUs in the group. | +| other_config | (string -> string) map | Additional optional configuration (as usual). | +| name_label | string | A human-readable name. | +| name_description | string | A notes field containing human-readable description. | +| GPU_types | string set | List of GPU types (vendor+device ID) that can be in this group (hidden field). | + +Messages: none. + +### VGPU class + +A virtual GPU device (vGPU). + +Fields: + +| Name | Type | Description | +|----------------------|--------------------------|--------------------------------------------------------------------------------------| +| uuid | string | Unique identifier/object reference. | +| VM | VM ref | VM that owns the vGPU. | +| GPU_group | GPU_group ref | GPU group the vGPU is contained in. | +| currently_attached | bool | Reflects whether the virtual device is currently "connected" to a physical device. | +| device | string | Order in which the devices are plugged into the VM. Restricted to "0" for now. | +| other_config | (string -> string) map | Additional optional configuration (as usual). + +Messages: + +| Prototype | Description | | +|---------------------------------------------------|--------------------------------------------------------------------------------------------------------|---| +| VGPU ref create (GPU_group ref, string, VM ref) | Manually assign the vGPU device to the VM given a device number, and link it to the given GPU group. | | +| void destroy (VGPU ref) | Remove the association between the GPU group and the VM. | | + +It is possible to assign more vGPUs to a group than number number of +pGPUs in the group. When a VM is started, a pGPU must be available; if +not, the VM will not start. Therefore, to guarantee that a VM has access +to a pGPU at any time, one must manually enforce that the number of +vGPUs in a GPU group does not exceed the number of pGPUs. XenCenter +might display a warning, or simply refuse to assign a vGPU, if this +constraint is violated. This is analogous to the handling of memory +availability in a pool: a VM may not be able to start if there is no +host having enough free memory. + +### VM class + +Fields: + +- Deprecate (unused) `PCI_bus` field +- Add field `VGPU ref set VGPUs`: List of vGPUs. +- Add field `PCI ref set attached_PCIs`: List of PCI devices that are + "currently attached" (plugged, passed-through) (*hidden field*). + +### host class + +Fields: + +- Add field `PCI ref set PCIs`: List of PCI devices. +- Add field `PGPU ref set PGPUs`: List of physical GPU devices. +- Add field `(string -> string) map chipset_info`, which contains at + least the key `iommu`. If `"true"`, this key indicates whether the + host has IOMMU/VT-d support build in, **and** this functionality is + enabled by Xen; the value will be `"false"` otherwise. + +Initialisation and Operations +----------------------------- + +### Enabling IOMMU/VT-d + +(This may not be needed in Xen 4.1. Confirm with Simon.) + +Provide a command that does this: + +- `/opt/xensource/libexec/xen-cmdline --set-xen iommu=1` +- reboot + +### Xapi startup + +Definitions: + +- PCI devices are matched on the combination of their `pci_id`, + `vendor_id`, and `device_id`. + +First boot and any subsequent xapi start: + +1. Find out from dmesg whether IOMMU support is present and enabled in + Xen, and set `host.chipset_info:iommu` accordingly. +2. Detect GPU devices currently present in the host. For each: + 1. If there is no matching PGPU object yet, create a PGPU object, + and add it to a GPU group containing identical PGPUs, or a new + group. + 2. If there is no matching PCI object yet, create one, and also + create or update the PCI objects for dependent devices. + +3. Destroy all existing PCI objects of devices that are not currently + present in the host (i.e. objects for devices that have been + replaced or removed). +4. Destroy all existing PGPU objects of GPUs that are not currently + present in the host. Send a XenAPI alert to notify the user of this + fact. +5. Update the list of `dependencies` on all PCI objects. +6. Sync `VGPU.currently_attached` on all `VGPU` objects. + +### Upgrade + +For any VMs that have `VM.other_config:pci` set to use a GPU, create an +appropriate vGPU, and remove the `other_config` option. + +### Generic PCI Interface + +A generic PCI interface exposed to higher-level code, such as the +networking and GPU management modules within Xapi. This functionality +relies on Xenops. + +The PCI module exposes the following functions: + +- Check whether a PCI device has free (unassigned) functions. This is + the case if the number of assignments in `PCI.attached_VMs` is + smaller than `PCI.functions`. +- Plug a PCI function into a running VM. + 1. Raise exception if there are no free functions. + 2. Plug PCI device, as well as dependent PCI devices. The PCI + module must also tell device-specific modules to update the + `currently_attached` field on dependent `VGPU` objects etc. + 3. Update `PCI.attached_VMs`. +- Unplug a PCI function from a running VM. + 1. Raise exception if the PCI function is not owned by (passed + through to) the VM. + 2. Unplug PCI device, as well as dependent PCI devices. The PCI + module must also tell device-specific modules to update the + `currently_attached` field on dependent `VGPU` objects etc. + 3. Update `PCI.attached_VMs`. + +### Construction and Destruction + +VGPU.create: + +1. Check license. Raise FEATURE\_RESTRICTED if the GPU feature has not + been enabled. +2. Raise INVALID\_DEVICE if the given device number is not "0", or + DEVICE\_ALREADY\_EXISTS if (indeed) the device already exists. This + is a convenient way of enforcing that only one vGPU per VM is + supported, for now. +3. Create `VGPU` object in the DB. +4. Initialise `VGPU.currently_attached = false`. +5. Return a ref to the new object. + +VGPU.destroy: + +1. Raise OPERATION\_NOT\_ALLOWED if `VGPU.currently_attached = true` + and the VM is running. +2. Destroy `VGPU` object. + +### VM Operations + +VM.start(\_on): + +1. If `host.chipset_info:iommu = "false"`, raise VM\_REQUIRES\_IOMMU. +2. Raise FEATURE\_REQUIRES\_HVM (carrying the string "GPU passthrough + needs HVM") if the VM is PV rather than HVM. +3. For each of the VM's vGPUs: + 1. Confirm that the given host has a pGPU in its associated GPU + group. If not, raise VM\_REQUIRES\_GPU. + 2. Consult the generic PCI module for all pGPUs in the group to + find out whether a suitable PCI function is available. If a + physical device is not available, raise VM\_REQUIRES\_GPU. + 3. Ask PCI module to plug an available pGPU into the VM's domain + and set `VGPU.currently_attached` to `true`. As a side-effect, + any dependent PCI devices would be plugged. + +VM.shutdown: + +1. Ask PCI module to unplug all GPU devices. +2. Set `VGPU.currently_attached` to `false` for all the VM's VGPUs. + +VM.suspend, VM.resume(\_on): + +- Raise VM\_HAS\_PCI\_ATTACHED if the VM has any plugged `VGPU` + objects, as suspend/resume for VMs with GPUs is currently not + supported. + +VM.pool\_migrate: + +- Raise VM\_HAS\_PCI\_ATTACHED if the VM has any plugged `VGPU` + objects, as live migration for VMs with GPUs is currently not + supported. + +VM.clone, VM.copy, VM.snapshot: + +- Copy `VGPU` objects along with the VM. + +VM.import, VM.export: + +- Include `VGPU` and `GPU_group` objects in the VM export format. + +VM.checkpoint + +- Raise VM\_HAS\_PCI\_ATTACHED if the VM has any plugged `VGPU` + objects, as checkpointing for VMs with GPUs is currently not + supported. + +### Pool Join and Eject + +Pool join: + +1. For each `PGPU`: + 1. Copy it to the pool. + 2. Add it to a `GPU_group` of identical PGPUs, or a new one. + +2. Copy each `VGPU` to the pool together with the VM that owns it, and + add it to the GPU group containing the same `PGPU` as before the + join. + +Step 1 is done automatically by the xapi startup code, and step 2 is +handled by the VM export/import code. Hence, no work needed. + +Pool eject: + +1. `VGPU` objects will be automatically GC'ed when the VMs are removed. +2. Xapi's startup code recreates the `PGPU` and `GPU_group` objects. + +Hence, no work needed. + +Required Low-level Interface +---------------------------- + +Xapi needs a way to obtain a list of all PCI devices present on a host. +For each device, xapi needs to know: + +- The PCI ID (BDF). +- The type of device (NIC, GPU, ...) according to a well-defined and + stable list of device types (as in `/usr/share/hwdata/pci.ids`). +- The device and vendor ID+name (currently, for PIFs, xapi looks up + the name in `/usr/share/hwdata/pci.ids`). +- Which other devices/functions are required to be passed through to + the same VM (co-located), e.g. other functions of a compound PCI + device. + +Command-Line Interface (xe) +----------------------------- + +- xe pgpu-list +- xe pgpu-param-list/get/set/add/remove/clear +- xe gpu-group-list +- xe gpu-group-param-list/get/set/add/remove/clear +- xe vgpu-list +- xe vgpu-create +- xe vgpu-destroy +- xe vgpu-param-list/get/set/add/remove/clear +- xe host-param-get param-name=chipset-info param-key=iommu + diff --git a/doc/content/design/gpu-support-evolution.md b/doc/content/design/gpu-support-evolution.md new file mode 100644 index 00000000000..73d1cb00d75 --- /dev/null +++ b/doc/content/design/gpu-support-evolution.md @@ -0,0 +1,209 @@ +--- +title: GPU support evolution +layout: default +design_doc: true +revision: 3 +status: released (7.0) +revision_history: +- revision_number: 1 + description: Documented interface changes between xapi and xenopsd for vGPU +- revision_number: 2 + description: Added design for storing vGPU-to-pGPU allocation in xapi database +- revision_number: 3 + description: Marked new xapi DB fields as internal-only +--- + +Introduction +------------ + +As of XenServer 6.5, VMs can be provisioned with access to graphics processors +(either emulated or passed through) in four different ways. Virtualisation of +Intel graphics processors will exist as a fifth kind of graphics processing +available to VMs. These five situations all require the VM's device model to be +created in subtly different ways: + +__Pure software emulation__ + +- qemu is launched either with no special parameter, if the basic Cirrus + graphics processor is required, otherwise qemu is launched with the + `-std-vga` flag. + +__Generic GPU passthrough__ + +- qemu is launched with the `-priv` flag to turn on privilege separation +- qemu can additionally be passed the `-std-vga` flag to choose the + corresponding emulated graphics card. + +__Intel integrated GPU passthrough (GVT-d)__ + +- As well as the `-priv` flag, qemu must be launched with the `-std-vga` and + `-gfx_passthru` flags. The actual PCI passthrough is handled separately + via xen. + +__NVIDIA vGPU__ + +- qemu is launched with the `-vgpu` flag +- a secondary display emulator, demu, is launched with the following parameters: + - `--domain` - the VM's domain ID + - `--vcpus` - the number of vcpus available to the VM + - `--gpu` - the PCI address of the physical GPU on which the emulated GPU will + run + - `--config` - the path to the config file which contains detail of the GPU to + emulate + +__Intel vGPU (GVT-g)__ + +- here demu is not used, but instead qemu is launched with five parameters: + - `-xengt` + - `-vgt_low_gm_sz` - the low GM size in MiB + - `-vgt_high_gm_sz` - the high GM size in MiB + - `-vgt_fence_sz` - the number of fence registers + - `-priv` + +xenopsd +------- + +To handle all these possibilities, we will add some new types to xenopsd's +interface: + +``` +module Pci = struct + type address = { + domain: int; + bus: int; + device: int; + fn: int; + } + + ... +end + +module Vgpu = struct + type gvt_g = { + physical_pci_address: Pci.address; + low_gm_sz: int64; + high_gm_sz: int64; + fence_sz: int; + } + + type nvidia = { + physical_pci_address: Pci.address; + config_file: string + } + + type implementation = + | GVT_g of gvt_g + | Nvidia of nvidia + + type id = string * string + + type t = { + id: id; + position: int; + implementation: implementation; + } + + type state = { + plugged: bool; + emulator_pid: int option; + } +end + +module Vm = struct + type igd_passthrough of + | GVT_d + + type video_card = + | Cirrus + | Standard_VGA + | Vgpu + | Igd_passthrough of igd_passthrough + + ... +end + +module Metadata = struct + type t = { + vm: Vm.t; + vbds: Vbd.t list; + vifs: Vif.t list; + pcis: Pci.t list; + vgpus: Vgpu.t list; + domains: string option; + } +end +``` + +The `video_card` type is used to indicate to the function +`Xenops_server_xen.VM.create_device_model_config` how the VM's emulated graphics +card will be implemented. A value of `Vgpu` indicates that the VM needs to be +started with one or more virtualised GPUs - the function will need to look at +the list of GPUs associated with the VM to work out exactly what parameters to +send to qemu. + +If `Vgpu.state.emulator_pid` of a plugged vGPU is `None`, this indicates that +the emulation of the vGPU is being done by qemu rather than by a separate +emulator. + +n.b. adding the `vgpus` field to `Metadata.t` will break backwards compatibility +with old versions of xenopsd, so some upgrade logic will be required. + +This interface will allow us to support multiple vGPUs per VM in future if +necessary, although this may also require reworking the interface between +xenopsd, qemu and demu. For now, xenopsd will throw an exception if it is asked +to start a VM with more than one vGPU. + +xapi +---- + +To support the above interface, xapi will convert all of a VM's non-passthrough +GPUs into `Vgpu.t` objects when sending VM metadata to xenopsd. + +In contrast to GVT-d, which can only be run on an Intel GPU which has been +has been hidden from dom0, GVT-g will only be allowed to run on a GPU which has +_not_ been hidden from dom0. + +If a GVT-g-capable GPU is detected, and it is not hidden from dom0, xapi will +create a set of VGPU_type objects to represent the vGPU presets which can run on +the physical GPU. Exactly how these presets are defined is TBD, but a likely +solution is via a set of config files as with NVIDIA vGPU. + +__Allocation of vGPUs to physical GPUs__ + +For NVIDIA vGPU, when starting a VM, each vGPU attached to the VM is assigned +to a physical GPU as a result of capacity planning at the pool level. The +resulting configuration is stored in the VM.platform dictionary, under +specific keys: + +- `vgpu_pci_id` - the address of the physical GPU on which the vGPU will run +- `vgpu_config` - the path to the vGPU config file which the emulator will use + +Instead of storing the assignment in these fields, we will add a new +internal-only database field: + +- `VGPU.scheduled_to_be_resident_on (API.ref_PGPU)` + +This will be set to the ref of the physical GPU on which the vGPU will run. From +here, xapi can easily obtain the GPU's PCI address. Capacity planning will also +take into account which vGPUs are scheduled to be resident on a physical GPU, +which will avoid races resulting from many vGPU-enabled VMs being started at +once. + +The path to the config file is already stored in the `VGPU_type.internal_config` +dictionary, under the key `vgpu_config`. xapi will use this value directly +rather than copying it to VM.platform. + +To support other vGPU implementations, we will add another internal-only +database field: + +- `VGPU_type.implementation enum(Passthrough|Nvidia|GVT_g)` + +For the `GVT_g` implementation, no config file is needed. Instead, +`VGPU_type.internal_config` will contain three key-value pairs, with the keys + +- `vgt_low_gm_sz` +- `vgt_high_gm_sz` +- `vgt_fence_sz` + +The values of these pairs will be used to construct a value of type +`Xenops_interface.Vgpu.gvt_g`, which will be passed down to xenopsd. diff --git a/doc/content/design/heterogeneous-pools.md b/doc/content/design/heterogeneous-pools.md new file mode 100644 index 00000000000..c3ca083f381 --- /dev/null +++ b/doc/content/design/heterogeneous-pools.md @@ -0,0 +1,289 @@ +--- +title: Heterogeneous pools +layout: default +design_doc: true +revision: 1 +status: released (5.6) +--- + +Notes +===== + +- The `cpuid` instruction is used to obtain a CPU's manufacturer, + family, model, stepping and features information. +- The feature bitvector is 128 bits wide: 2 times 32 bits of base + features plus 2 times 32 bits of extended features, which are + referred to as `base_ecx`, `base_edx`, `ext_ecx` and `ext_edx` + (after the registers used by `cpuid` to store the results). +- The feature bits can be masked by Intel FlexMigration and AMD + Extended Migration. This means that features can be made to appear + as absent. Hence, a CPU can appear as a less-capable CPU. + - AMD Extended Migration is able to mask both base and extended + features. + - Intel FlexMigration on Core 2 CPUs (Penryn) is able to mask + **only the base features** (`base_ecx` and `base_edx`). The + newer Nehalem and Westmere CPUs support extended-feature masking + as well. +- A process in dom0 (e.g. xapi) is able to call `cpuid` to obtain the + (possibly modified) CPU info, or can obtain this information from + Xen. Masking is done only by Xen at boot time, before any domains + are loaded. +- To apply a feature mask, a dom0 process may specify the mask in the + Xen command line in the file `/boot/extlinux.conf`. After a reboot, + the mask will be enforced. +- It is not possible to obtain the original features from a dom0 + process, if the features have been masked. Before applying the first + mask, the process could remember/store the original feature vector, + or obtain the information from Xen. +- All CPU cores on a host can be assumed to be identical. Masking will + be done simultaneously on all cores in a host. +- Whether a CPU supports FlexMigration/Extended Migration can (only) + be derived from the family/model/stepping information. +- XS5.5 has an exception for the EST feature in base\_ecx. This flag + is ignored on pool join. + +Overview of XenAPI Changes +========================== + +Fields +------ + +Currently, the datamodel has `Host_cpu` objects for each CPU core in a +host. As they are all identical, we are considering keeping just one CPU +record in the `Host` object itself, and deprecating the `Host_cpu` +class. For backwards compatibility, the `Host_cpu` objects will remain +as they are in MNR, but may be removed in subsequent releases. + +Hence, there will be a new field called `Host.cpu_info`, a read-only +string-string map, containing the following fixed set of keys: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Key nameDescription
cpu_countThe number of CPU cores in the host.
familyThe family (number) of the CPU.
featuresThe current (possibly masked) feature vector, as given by cpuid. Format: "<base_ecx>-<base_edx>-<ext_ecx>-<ext_edx>", 4 groups of 8 hexadecimal digits, separated by dashes.
features_after_rebootThe feature vector to be used after rebooting the host. This field can be modified by calling Host.set_cpu_features. Same format as features.
flagsThe flags of the physical CPU (a decoded version of the features field).
maskableIndicating whether the CPU supports Intel FlexMigration or AMD Extended Migration. There are three possible values: "no" means that masking is not possible, "base" means that only base features can be masked, and "full" means that base as well as extended features can be masked.
modelThe model number of the CPU.
modelnameThe model name of the CPU.
physical_featuresThe original, unmasked features. Same format as features.
speedThe speed of the CPU.
steppingThe stepping of the CPU.
vendorThe manufacturer of the CPU.
+ +Indicating whether the CPU supports Intel FlexMigration or AMD Extended +Migration. There are three possible values: `"no"` means that masking is +not possible, `"base"` means that only base features can be masked, and +`"full"` means that base as well as extended features can be masked. + +Note: When the `features` and `features_after_reboot` are different, +XenCenter could display a warning saying that a reboot is needed to +enforce the feature masking. + +The `Pool.other_config:cpuid_feature_mask` key is recognised. If this +key is present and if it contains a value in the same format as +`Host.cpu_info:features`, the value is used to mask the feature vectors +before comparisons during any pool join in the pool it is defined on. +This can be used to white-list certain feature flags, i.e. to ignore +them when adding a new host to a pool. The default it +`ffffff7f-ffffffff-ffffffff-ffffffff`, which white-lists the EST feature +for compatibility with XS 5.5 and earlier. + +Messages +-------- + +New messages: + +- `Host.set_cpu_features` + - Parameters: Host reference `host`, new CPU feature vector + `features`. + - Roles: only Pool Operator and Pool Admin. + - Sets the feature vector to be used after a reboot + (`Host.cpu_info:features_after_reboot`), if `features` is valid. +- `Host.reset_cpu_features` + - Parameter: Host reference `host`. + - Roles: only Pool Operator and Pool Admin. + - Removes the feature mask, such that after a reboot all features + of the CPU are enabled. + +XAPI +==== + +Back-end +-------- + +- Xen keeps the physical (unmasked) CPU features in memory when + starts, before applying any masks. Xen exposes the physical + features, as well as the current (possibly masked) features, to + dom0/xapi via the function `xc_get_boot_cpufeatures` in libxc. +- A dom0 script `/etc/xensource/libexec/xen-cmdline`, which provides a + future-proof way of modifying the Xen command-line key/value pairs. + This script has the following options, where `mask` is one of + `cpuid_mask_ecx`, `cpuid_mask_edx`, `cpuid_mask_ext_ecx` or + `cpuid_mask_ext_edx`, and `value` is `0xhhhhhhhh` (`h` is represents + a hex digit).: + - `--list-cpuid-masks` + - `--set-cpuid-masks mask=value mask=value` + - `--delete-cpuid-masks mask mask` +- A `restrict_cpu_masking` key has been added to the host licensing + restrictions map. This will be `true` when the `Host.edition` is + `free`, and `false` if it is `enterprise` or `platinum`. + +Start-up +-------- + +The `Host.cpu_info` field is refreshed: + +- The values for the keys `cpu_count`, `vendor`, `speed`, `modelname`, + `flags`, `stepping`, `model`, and `family` are obtained from + `/etc/xensource/boot_time_cpus` (and ultimately from + `/proc/cpuinfo`). +- The values of the `features` and `physical_features` are obtained + from Xen and the `features_after_reboot` key is made equal to the + `features` field. +- The value of the `maskable` key is determined by the CPU details. + - for Intel Core2 (Penryn) CPUs: + `family = 6 and (model = 1dh or (model = 17h and stepping >= 4))` + (`maskable = "base"`) + - for Intel Nehalem/Westmere CPUs: + `family = 6 and ((model = 1ah and stepping > 2) or model = 1eh or model = 25h or model = 2ch or model = 2eh or model = 2fh)` + (`maskable = "full"`) + - for AMD CPUs: `family >= 10h` (`maskable = "full"`) + +Setting (Masking) and Resetting the CPU Features +------------------------------------------------ + +- The `Host.set_cpu_features` call: + - checks whether the license of the host is Enterprise or + Platinum; throws FEATURE\_RESTRICTED if not. + - expects a string of 32 hexadecimal digits, optionally containing + spaces; throws INVALID\_FEATURE\_STRING if malformed. + - checks whether the given feature vector can be formed by masking + the physical feature vector; throws INVALID\_FEATURE\_STRING if + not. Note that on Intel Core 2 CPUs, it is only possible to the + mask the base features! + - checks whether the CPU supports FlexMigration/Extended + Migration; throws CPU\_FEATURE\_MASKING\_NOT\_SUPPORTED if not. + - sets the value of `features_after_reboot` to the given feature + vector. + - adds the new feature mask to the Xen command-line via the + `xen-cmdline` script. The mask is represented by one or more of + the following key/value pairs (where `h` represents a hex + digit): + - `cpuid_mask_ecx=0xhhhhhhhh` + - `cpuid_mask_edx=0xhhhhhhhh` + - `cpuid_mask_ext_ecx=0xhhhhhhhh` + - `cpuid_mask_ext_edx=0xhhhhhhhh` +- The `Host.reset_cpu_features` call: + - copies `physical_features` to `features_after_reboot`. + - removes the feature mask from the Xen command-line via the + `xen-cmdline` script (if any). + +Pool Join and Eject +------------------- + +- `Pool.join` fails when the `vendor` and `feature` keys do not match, + and disregards any other key in `Host.cpu_info`. + - However, as XS5.5 disregards the EST flag, there is a new way to + disregard/ignore feature flags on pool join, by setting a mask + in `Pool.other_config:cpuid_feature_mask`. The value of this + field should have the same format as `Host.cpu_info:features`. + When comparing the CPUID features of the pool and the joining + host for equality, this mask is applied before the comparison. + The default is `ffffff7f-ffffffff-ffffffff-ffffffff`, which + defines the EST feature, bit 7 of the base ecx flags, as "don't + care". +- `Pool.eject` clears the database (as usual), and additionally + removes the feature mask from `/boot/extlinux.conf` (if any). + +CLI +=== + +New commands: + +- `host-cpu-info` + - Parameters: `uuid` (optional, uses localhost if absent). + - Lists `Host.cpu_info` associated with the host. +- `host-get-cpu-features` + - Parameters: `uuid` (optional, uses localhost if absent). + - Returns the value of `Host.cpu_info:features]` associated with + the host. +- `host-set-cpu-features` + - Parameters: `features` (string of 32 hexadecimal digits, + optionally containing spaces or dashes), `uuid` (optional, uses + localhost if absent). + - Calls `Host.set_cpu_features`. +- `host-reset-cpu-features` + - Parameters: `uuid` (optional, uses localhost if absent). + - Calls `Host.reset_cpu_features`. + +The following commands will be deprecated: `host-cpu-list`, +`host-cpu-param-get`, `host-cpu-param-list`. + +WARNING: + +If the user is able to set any mask they like, they may end up disabling +CPU features that are required by dom0 (and probably other guest OSes), +resulting in a kernel panic when the machine restarts. Hence, using the +set function is potentially dangerous. + +It is apparently not easy to find out exactly which flags are safe to +mask and which aren't, so we cannot prevent an API/CLI user from making +mistakes in this way. However, using XenCenter would always be safe, as +XC always copies features masks from real hosts. + +If a machine ends up in such a bad state, there is a way to get out of +it. At the boot prompt (before Xen starts), you can type "menu.c32", +select a boot option and alter the Xen command-line to remove the +feature masks, after which the machine will again boot normally (note: +in our set-up, there is first a PXE boot prompt; the second prompt is +the one we mean here). + +The API/CLI documentation should stress the potential danger of using +this functionality, and explain how to get out of trouble again. + diff --git a/doc/content/design/integrated-gpu-passthrough/index.md b/doc/content/design/integrated-gpu-passthrough/index.md new file mode 100644 index 00000000000..4b9a827ef5d --- /dev/null +++ b/doc/content/design/integrated-gpu-passthrough/index.md @@ -0,0 +1,95 @@ +--- +title: Integrated GPU passthrough support +layout: default +design_doc: true +revision: 3 +status: released (6.5 SP1) +design_review: 33 +--- + +Introduction +------------ + +Passthrough of discrete GPUs has been +[available since XenServer 6.0]({{site.baseurl}}/xapi/design/gpu-passthrough.html). +With some extensions, we will also be able to support passthrough of integrated +GPUs. + +- Whether an integrated GPU will be accessible to dom0 or available to + passthrough to guests must be configurable via XenAPI. +- Passthrough of an integrated GPU requires an extra flag to be sent to qemu. + +Host Configuration +------------------ + +New fields will be added (both read-only): + +- `PGPU.dom0_access enum(enabled|disable_on_reboot|disabled|enable_on_reboot)` +- `host.display enum(enabled|disable_on_reboot|disabled|enable_on_reboot)` + +as well as new API calls used to modify the state of these fields: + +- `PGPU.enable_dom0_access` +- `PGPU.disable_dom0_access` +- `host.enable_display` +- `host.disable_display` + +Each of these API calls will return the new state of the field e.g. calling +`host.disable_display` on a host with `display = enabled` will return +`disable_on_reboot`. + +Disabling dom0 access will modify the xen commandline (using the xen-cmdline +tool) such that dom0 will not be able to access the GPU on next boot. + +Calling host.disable_display will modify the xen and dom0 commandlines such +that neither will attempt to send console output to the system display device. + +A state diagram for the fields `PGPU.dom0_access` and `host.display` is shown +below: + +![host.integrated_GPU_passthrough flow diagram](integrated-gpu-passthrough.png) + +While it is possible for these two fields to be modified independently, a +client must disable both the host display and dom0 access to the system display +device before that device can be passed through to a guest. + +Note that when a client enables or disables either of these fields, the change +can be cancelled until the host is rebooted. + +Handling vga_arbiter +-------------------- + +Currently, xapi will not create a PGPU object for the PCI device with address +reported by `/dev/vga_arbiter`. This is to prevent a GPU in use by dom0 from +from being passed through to a guest. This behaviour will be changed - instead +of not creating a PGPU object at all, xapi will create a PGPU, but its +supported_VGPU_types field will be empty. + +However, the PGPU's supported_VGPU_types will be populated as normal if: + +1. dom0 access to the GPU is disabled. +2. The host's display is disabled. +3. The vendor ID of the device is contained in a whitelist provided by xapi's + config file. + +A read-only field will be added: + +- `PGPU.is_system_display_device bool` + +This will be true for a PGPU iff `/dev/vga_arbiter` reports the PGPU as the +system display device for the host on which the PGPU is installed. + +Interfacing with xenopsd +------------------------ + +When starting a VM attached to an integrated GPU, the VM config sent to xenopsd +will contain a video_card of type IGD_passthrough. This will override the type +determined from VM.platform:vga. xapi will consider a GPU to be integrated if +both: + +1. It resides on bus 0. +2. The vendor ID of the device is contained in a whitelist provided by xapi's + config file. + +When xenopsd starts qemu for a VM with a video_card of type IGD_passthrough, +it will pass the flags "-std-vga" AND "-gfx_passthru". diff --git a/doc/content/design/integrated-gpu-passthrough/integrated-gpu-passthrough.png b/doc/content/design/integrated-gpu-passthrough/integrated-gpu-passthrough.png new file mode 100644 index 0000000000000000000000000000000000000000..0d3b28a3de681b90167536c13177fe1204f3e9d0 GIT binary patch literal 20590 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 literal 0 HcmV?d00001 diff --git a/doc/content/design/local-database.md b/doc/content/design/local-database.md new file mode 100644 index 00000000000..2393df63760 --- /dev/null +++ b/doc/content/design/local-database.md @@ -0,0 +1,68 @@ +--- +title: Local database +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +All hosts in a pool use the shared database by sending queries to +the pool master. This creates a performance bottleneck as the pool +size increases. All hosts in a pool receive a database backup from +the master periodically, every couple of hours. This creates a +reliability problem as updates may be lost if the master fails during +the window before the backup. + +The reliability problem can be avoided by running with HA or the redo +log enabled, but this is not always possible. + +We propose to: + +- adapt the existing event machinery to allow every host to maintain + an up-to-date database replica; +- actively cache the database locally on each host and satisfy read + operations from the cache. Most database operations are reads so + this should reduce the number of RPCs across the network. + +In a later phase we can move to a completely +[distributed database](../distributed-database). + +Replicating the database +------------------------ + +We will create a database-level variant of the existing XenAPI `event.from` +API. The new RPC will block until a database event is generated, and then +the events will be returned using the existing "redo-log" event types. We +will add a few second delay into the RPC to batch the updates. + +We will replace the pool database download logic with an `event.from`-like +loop which fetches all the events from the master's database and applies +them to the local copy. The first call will naturally return the full database +contents. + +We will turn on the existing "in memory db cache" mechanism on all hosts, +not just the master. This will be where the database updates will go. + +The result should be that every host will have a `/var/xapi/state.db` file, +with writes going to the master first and then filtering down to all slaves. + +Using the replica as a cache +---------------------------- + +We will re-use the [Disaster Recovery](../../toolstack/features/DR) multiple +database mechanism to allow slaves to access their local database. We will +change the defalult database "context" to snapshot the local database, +perform reads locally and write-through to the master. + +We will add an HTTP header to all forwarded XenAPI calls from the master which +will include the current database generation count. When a forwarded XenAPI +operation is received, the slave will deliberately wait until the local cache +is at least as new as this, so that we always use fresh metadata for XenAPI +calls (e.g. the VM.start uses the absolute latest VM memory size). + +We will document the new database coherence policy, i.e. that writes on a host +will not immediately be seen by reads on another host. We believe that this +is only a problem when we are using the database for locking and are attempting +to hand over a lock to another host. We are already using XenAPI calls forwarded +to the master for some of this, but may need to do a bit more of this; in +particular the storage backends may need some updating. diff --git a/doc/content/design/management-interface-on-vlan.md b/doc/content/design/management-interface-on-vlan.md new file mode 100644 index 00000000000..b9af46c91fd --- /dev/null +++ b/doc/content/design/management-interface-on-vlan.md @@ -0,0 +1,224 @@ +--- +title: Management Interface on VLAN +layout: default +design_doc: true +revision: 3 +status: proposed +revision_history: +- revision_number: 1 + description: Initial version +- revision_number: 2 + description: Addition of `networkd_db` update for Upgrade +- revision_number: 3 + description: More info on `networkd_db` and API Errors +--- + +This document describes design details for the +REQ-42: Support Use of VLAN on XAPI Management Interface. + +XAPI and XCP-Networkd +=============== + +### Creating a VLAN + +Creating a VLAN is already there, Lisiting the steps to create a VLAN which is used later in the document. +Steps: + +1. Check the PIFs created on a Host for physical devices `eth0`, `eth1`. + `xe pif-list params=uuid physical=true host-uuid=UUID` this will list `pif-UUID` +2. Create a new network for the VLAN interface. + `xe network-create name-label=VLAN1` + It returns a new `network-UUID` +3. Create a VLAN PIF. + `xe vlan-create pif-uuid=pif-UUID network-uuid=network-UUID vlan=VLAN-ID` + It returns a new VLAN PIF `new-pif-UUID` +4. Plug the VLAN PIF. + `xe pif-plug uuid=new-pif-UUID` +5. Configure IP on the VLAN PIF. + `xe pif-reconfigure-ip uuid=new-pif-UUID mode= IP= netmask= gateway= DNS= ` + This will configure IP on the PIF, here `mode` is must and other parametrs are needed on selecting mode=static + +Similarly, creating a vlan pif can be achieved by corresponding XenAPI calls. + +Recognise VLAN config from management.conf +---------------------------------------------- + +For a newly installed host, If host installer was asked to put the management interface on given VLAN. +We will expect a new entry `VLAN=ID` under `/etc/firstboot.d/data/management.conf`. + +Listing current contents of management.conf which will be used later in the document. +`LABEL`=`eth0` -> Represents Pyhsical device on which Management Interface must reside. +`MODE`=`dhcp`||`static` -> Represents IP configuration mode for the Management Interface. There can be other parameters like IP, NETMASK, GATEWAY and DNS when we have `static` mode. +`VLAN`=`ID` -> New entry for specifying VLAN TAG going to be configured on device `LABEL`. + Management interface going to be configured on this VLAN ID with specified mode. + +### Firstboot script need to recognise VLAN config + +Firstboot script `/etc/firstboot.d/30-prepare-networking` need to be updated for configuring +management interface to be on provided VLAN ID. + +Steps to be followed: + +1. `PIF.scan` performed in the script must have created the PIFs for the underlying pyhsical devices. +2. Get the PIF UUID for physical device `LABEL`. +3. Repeat the steps mentioned in `Creating a VLAN`, i.e. network-create, vlan-create and pif-plug. Now we have a new PIF for the VLAN. +4. Perform `pif-reconfigure-ip` for the new VLAN PIF. +5. Perform `host-management-reconfigure` using new VLAN PIF. + +### XCP-Networkd need to recognise VLAN config during startup + +XCP-Networkd during first boot and boot after pool eject gets the initial network setup from the `management.conf` and `xensource-inventory` file to update the network.db for management interface info. +XCP-Networkd must honour the new VLAN config. + +Steps to be followed: + +1. During startup `read_config` step tries to read the `/var/lib/xcp/networkd.db` file which is not yet created just after host installation. +2. Since `networkd.db` read throws `Read_Error`, it tries to read `network.dbcache` which is also not available hence it goes to read `read_management_conf` file. +3. There can be two possible MODE `static` or `dhcp` taken from management.conf. +4. `bridge_name` is taken as `MANAGEMENT_INTERFACE` from xensource-inventory, further `bridge_config` and `interface_config` are build based on MODE. +5. Call `Bridge.make_config()` and `Interface.make_config()` are performed with respective `bridge_config` and `interface_config`. + +Updating networkd_db program +---------------------------- + +`networkd_db` provides the management interface info to the host installer during upgrade. +It reads `/var/lib/xcp/networkd.db` file to output the Management Interface information. Here we need to update the networkd_db to output the VLAN information when vlan bridge is a input. + +Steps to be followed: + +1. Currently VLAN interface IP information is provided correctly on passing VLAN bridge as input. + `networkd_db -iface xapi0` this will list `mode` as dhcp or static, if mode=static then it will provide `ipaddr` and `netmask` too. +2. We need to udpate this program to provide VLAN ID and parent bridge info on passing VLAN bridge as input. + `networkd_db -bridge xapi0` It should output the VLAN info like: + `interfaces=` + `vlan=vlanID` + `parent=xenbr0` using the parent bridge user can identify the physical interfaces. + Here we will extract VLAN and parent bridge from `bridge_config` under `networkd.db`. + +Additional VLAN parameter for Emergency Network Reset +----------------------------------------------------- + +Detail design is mentioned on http://xapi-project.github.io/xapi/design/emergency-network-reset.html +For using `xe-reset-networking` utility to configure management interface on VLAN, We need to add one more parameter `--vlan=vlanID` to the utility. +There are certain parameters need to be passed to this utility: --master, --device, --mode, --ip, --netmask, --gateway, --dns and new one --vlan. + +### VLAN parameter addition to xe-reset-networking + +Steps to be followed: + +1. Check if `VLANID` is passed then let bridge=`xapi0`. +2. Write the `bridge=xapi0` into xensource-inventory file, This should work as Xapi check avialable bridges while creating networks. +3. Write the `VLAN=vlanID` into `management.conf` and `/tmp/network-reset`. +4. Modify `check_network_reset` under xapi.ml to perform steps `Creating a VLAN` and perform `management_reconfigure` on vlan pif. + Step `Creating a VLAN` must have created the VLAN record in Xapi DB similar to firstboot script. +5. If no VLANID is specified then retain the current one, This utility must take the management interface info from `networkd_db` program and handle the VLAN config. + +### VLAN parameter addition to xsconsole Emergency Network Reset + +Under `Emergency Network Reset` option under the `Network and Management Interface` menu. +Selecting this option will show some explanation in the pane on the right-hand side. +Pressing will bring up a dialogue to select the interfaces to use as management interface after the reset. +After choosing a device, the dialogue continues with configuration options like in the `Configure Management Interface` dialogue. +There will be an additionall option for VLAN in the dialogue. +After completing the dialogue, the same steps as listed for xe-reset-networking are executed. + +Updating Pool Join/Eject operations +----------------------------------- + +### Pool Join while Pool having Management Interface on a VLAN + +Currently `pool-join` fails if VLANs are present on the host joining a pool. +We need to allow pool-join only if Pool and host joining a pool both has management interface on same VLAN. + +Steps to be followed: + +1. Under `pre_join_checks` update function `assert_only_physical_pifs` to check Pool master management_interface is on same VLAN. +2. Call `Host.get_management_interface` on Pool master and get the vlanID, match it with `localhost` management_interface VLAN ID. + If it matches then allow pool-join. +3. In case if there are multiple VLANs on host joining a pool, fail the pool-join gracefully. +4. After the pool-join, Host xapi db will get sync from pool master xapi db, This will be fine to have management interface on VLAN. + +### Pool Eject while host ejected having Management Interface on a VLAN + +Currently managament interface VLAN config on host is not been retained in `xensource-inventory` or `management.conf` file. +We need to retain the vlanID under config files. + +Steps to be followed: + +1. Under call `Pool.eject` we need to update `write_first_boot_management_interface_configuration_file` function. +2. Check if management_interface is on VLAN then get the VLANID from the pif. +3. Update the VLANID into the `managament.conf` file and the `bridge` into `xensource-inventory` file. + In order to be retained by XCP-Networkd on startup after the host is ejected. + +New API for Pool Management Reconfigure +--------------------------------------- + +Currently there is no Pool Level API to reconfigure management_interface for all of the Hosts in a Pool at once. +API `Pool.management_reconfigure` will be needed in order to reconfigure `manamegemnt_interface` on all hosts in a Pool to the same Network either VLAN or Physical. + + +### Current behaviour to change the Management Interface on Host + +Currently call `Host.management_reconfigure` with VLAN pif-uuid can change the management_interface to specified VLAN. +Listing the steps to understand the workflow of `management_interface` reconfigure. We will be using `Host.management_reconfigure` call inside the new API. + +Steps performed during management_reconfigure: + +1. `bring_pif_up` get called for the pif. +2. `xensource-inventory` get updated with the latest info of interface. +3 `update-mh-info` updates the management_mac into xenstore. +4. Http server gets restarted, even though xapi listen on all IP addresses, This new interface as `_the_ management` interface is used by slaves to connect to pool master. +5. `on_dom0_networking_change` refreshes console URIs for the new IP address. +6. Xapi db is updated with new management interface info. + +### Management Reconfigure on Pool from Physical Network to VLAN Network or from VLAN Network to Other VLAN Network or from VLAN Network to Physical Network + +Listing steps to be performed manually on each Host or Pool as a prerequisite to use the New API. +We need to make sure that new network which is going to be a management interface has PIFs configured on each Host. +In case of pyhsical network we will assume pifs are configured on each host, In case of vlan network we need to create vlan pifs on each Host. +We would assume that VLAN is available on the switch/network. + +Manual steps to be performed before calling new API: + +1. Create a vlan network on pool via `network.create`, In case of pyhsical NICs network must be present. +2. Create a vlan pif on each host via `VLAN.create` using above network ref, physical PIF ref and vlanID, Not needed in case of pyhsical network. + Or An Alternate call `pool.create_VLAN` providing `device` and above `network` will create vlan PIFs for all hosts in a pool. +3. Perform `PIF.reconfigure_ip` for each new Network PIF on each Host. + +If User wishes to change the management interface manually on each Host in a Pool, We should allow it, There will be a guideline for that: + +User can individually change management interface on each host calling `Host.management_reconfigure` using pifs on physical devices or vlan pifs. +This must be perfomed on slaves first and lastly on Master, As changing management_interface on master will disconnect slaves from master then further calls `Host.management_reconfigure` cannot be performed till master recover slaves via call `pool.recover_slaves`. + +### API Details + +- `Pool.management_reconfigure` + - Parameter: network reference `network`. + - Calling this function configures `management_interface` on each host of a pool. + - For the `network` provided it will check pifs are present on each Host, + In case of VLAN network it will check vlan pifs on provided network are present on each Host of Pool. + - Check IP is configured on above pifs on each Host. + - If PIFs are not present or IP is not configured on PIFs this call must fail gracefully, Asking user to configure them. + - Call `Host.management_reconfigure` on each slave then lastly on master. + - Call `pool.recover_slaves` on master inorder to recover slaves which might have lost the connection to master. + +### API errors + +Possible API errors that may be raised by `pool.management_reconfigure`: + +- `INTERFACE_HAS_NO_IP` : the specified PIF (`pif` parameter) has no IP configuration. The new API checks for all PIFs on the new Network has IP configured. There might be a case when user has forgotten to configure IP on PIF on one or many of the Hosts in a Pool. + +New API ERROR: + +- `REQUIRED_PIF_NOT_PRESENT` : the specified Network (`network` parameter) has no PIF present on the host in pool. There might be a case when user has forgotten to create vlan pif on one or many of the Hosts in a Pool. + +CP-Tickets +---------- + +1. CP-14027 +2. CP-14028 +3. CP-14029 +4. CP-14030 +5. CP-14031 +6. CP-14032 +7. CP-14033 diff --git a/doc/content/design/multiple-cluster-managers.md b/doc/content/design/multiple-cluster-managers.md new file mode 100644 index 00000000000..6c0e783fe66 --- /dev/null +++ b/doc/content/design/multiple-cluster-managers.md @@ -0,0 +1,73 @@ +--- +title: Multiple Cluster Managers +layout: default +design_doc: true +revision: 2 +status: confirmed +revision_history: +- revision_number: 1 + description: Initial revision +- revision_number: 2 + description: Short-term simplications and scope reduction +--- + +Introduction +------------ + +Xapi currently uses a cluster manager called [xhad](../../features/HA/HA.html). Sometimes other software comes with its own built-in way of managing clusters, which would clash with xhad (example: xhad could choose to fence node 'a' while the other system could fence node 'b' resulting in a total failure). To integrate xapi with this other software we have 2 choices: + +1. modify the other software to take membership information from xapi; or +2. modify xapi to take membership information from this other software. + +This document proposes a way to do the latter. + +XenAPI changes +-------------- + +### New field + +We will add the following new field: + +- `pool.ha_cluster_stack` of type `string` (read-only) + - If HA is enabled, this field reflects which cluster stack is in use. + - Set to `"xhad"` on upgrade, which implies that so far we have used XenServer's own cluster stack, called `xhad`. + +### Cluster-stack choice + +We assume for now that a particular cluster manager will be mandated (only) by certain types of clustered storage, recognisable by SR type (e.g. OCFS2 or Melio). The SR backend will be able to inform xapi if the SR needs a particular cluster stack, and if so, what is the name of the stack. + +When `pool.enable_ha` is called, xapi will determine which cluster stack to use based on the presence or absence of such SRs: + +- If an SR that needs its own cluster stack is attached to the pool, then xapi will use that cluster stack. +- If no SR that needs a particular cluster stack is attached to the pool, then xapi will use `xhad`. + +If multiple SRs that need a particular cluster stack exist, then the storage parts of xapi must ensure that no two such SRs are ever attached to a pool at the same time. + +### New errors + +We will add the following API error that may be raised by `pool.enable_ha`: + +- `INCOMPATIBLE_STATEFILE_SR`: the specified SRs (`heartbeat_srs` parameter) are not of the right type to hold the HA statefile for the `cluster_stack` that will be used. For example, there is a Melio SR attached to the pool, and therefore the required cluster stack is the Melio one, but the given heartbeat SR is not a Melio SR. The single parameter will be the name of the required SR type. + +The following new API error may be raised by `PBD.plug`: + +- `INCOMPATIBLE_CLUSTER_STACK_ACTIVE`: the operation cannot be performed because an incompatible cluster stack is active. The single parameter will be the name of the required cluster stack. This could happen (or example) if you tried to create an OCFS2 SR with XenServer HA already enabled. + +### Future extensions + +In future, we may add a parameter to explicitly choose the cluster stack: + +- New parameter to `pool.enable_ha` called `cluster_stack` of type `string` which will have the default value of empty string (meaning: let the implementation choose). +- With the additional parameter, `pool.enable_ha` may raise two new errors: + - `UNKNOWN_CLUSTER_STACK`: + The operation cannot be performed because the requested cluster stack does not exist. The user should check the name was entered correctly and, failing that, check to see if the software is installed. The exception will have a single parameter: the name of the cluster stack which was not found. + - `CLUSTER_STACK_CONSTRAINT`: HA cannot be enabled with the provided cluster stack because some third-party software is already active which requires a different cluster stack setting. The two parameters are: a reference to an object (such as an SR) which has created the restriction, and the name of the cluster stack that this object requires. + +Implementation +-------------- + +The `xapi.conf` file will have a new field: `cluster-stack-root` which will have the default value `/usr/libexec/xapi/cluster-stack`. The existing `xhad` scripts and tools will be moved to `/usr/libexec/xapi/cluster-stack/xhad/`. A hypothetical cluster stack called `foo` would be placed in `/usr/libexec/xapi/cluster-stack/foo/`. + +In `Pool.enable_ha` with `cluster_stack="foo"` we will verify that the subdirectory `/foo` exists. If it does not exist, then the call will fail with `UNKNOWN_CLUSTER_STACK`. + +Alternative cluster stacks will need to conform to the exact same interface as [xhad](../../features/HA/HA.html). diff --git a/doc/content/design/multiple-device-emulators.md b/doc/content/design/multiple-device-emulators.md new file mode 100644 index 00000000000..ae0225fb452 --- /dev/null +++ b/doc/content/design/multiple-device-emulators.md @@ -0,0 +1,72 @@ +--- +title: Multiple device emulators +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +Xen's `ioreq-server` feature allows for several device emulator +processes to be attached to the same domain, each emulating different +sets of virtual hardware. This makes it possible, for example, to +emulate network devices in a separate process for improved security +and isolation, or to provide special purpose emulators for particular +virtual hardware devices. + +`ioreq-server` is currently used in XenServer to support vGPU, where it +is configured via the legacy toolstack interface. These changes will make +multiple emulators usable in open source Xen via the new libxl interface. + +libxl changes +------------- + +- The singleton device_model_version, device_model_stubdomain and + device_model fields in the b_info structure will be replaced by a list of + (version, stubdomain, model, arguments) tuples, one for each emulator. + +- libxl_domain_create_new() will be changed to spawn a new device model + for each entry in the list. + +It may also be useful to spawn the device models separately and only +attach them during domain creation. This could be supported by +making each device_model entry a union of `pid | parameter_tuple`. +If such an entry specifies a parameter tuple, it is processed as above; +if it specifies a pid, libxl_domain_create_new(), the existing device +model with that pid is attached instead. + +QEMU changes +------------ + +- Patches to make QEMU register with Xen as an ioreq-server have been + submitted upstream, but not yet applied. + +- QEMU's `--machine none` and `--nodefaults` options should make it + possible to create an empty machine and add just a host bus, PCI bus + and device. This has not yet been fully demonstrated, so QEMU changes + may be required. + +Xen changes +----------- + +- Until now, `ioreq-server` has only been used to connect one extra + device model, in addition to the default one. Multiple emulators + should work, but there is a chance that bugs will be discovered. + +Interfacing with xenopsd +------------------------ + +This functionality will only be available through the experimental +Xenlight-based xenopsd. + + - the `VM_build` clause in the `atomics_of_operation` function will be + changed to fill in the list of emulators to be created (or attached) + in the b_info struct + +Host Configuration +------------------ + +vGPU support is implemented mostly in xenopsd, so no Xapi changes are +required to support vGPU through the generic device model mechanism. +Changes would be required if we decided to expose the additional device +models through the API, but in the near future it is more likely that +any additional device models will be dealt with entirely by xenopsd. diff --git a/doc/content/design/ocfs2/index.md b/doc/content/design/ocfs2/index.md new file mode 100644 index 00000000000..c8d0852e0a9 --- /dev/null +++ b/doc/content/design/ocfs2/index.md @@ -0,0 +1,491 @@ +--- +title: OCFS2 storage +layout: default +design_doc: true +revision: 1 +status: proposed +--- + + +OCFS2 is a (host-)clustered filesystem which runs on top of a shared raw block +device. Hosts using OCFS2 form a cluster using a combination of network and +storage heartbeats and host fencing to avoid split-brain. + +The following diagram shows the proposed architecture with `xapi`: + +![Proposed architecture](ocfs2.png) + +Please note the following: + +- OCFS2 is configured to use global heartbeats rather than per-mount heartbeats + because we quite often have many SRs and therefore many mountpoints +- The OCFS2 global heartbeat should be collocated on the same SR as the XenServer + HA SR so that we depend on fewer SRs (the storage is a single point of failure + for OCFS2) +- The OCFS2 global heartbeat should itself be a raw VDI within an LVHDSR. +- Every host can be in at-most-one OCFS2 cluster i.e. the host cluster membership + is a per-host thing rather than a per-SR thing. Therefore `xapi` will be + modified to configure the cluster and manage the cluster node numbers. +- Every SR will be a filesystem mount, managed by a SM plugin called "OCFS2". +- Xapi HA uses the `xhad` process which runs in userspace but in the realtime + scheduling class so it has priority over all other userspace tasks. `xhad` + sends heartbeats via the `ha_statefile` VDI and via UDP, and uses the + Xen watchdog for host fencing. +- OCFS2 HA uses the `o2cb` kernel driver which sends heartbeats via the + `o2cb_statefile` and via TCP, fencing the host by panicing domain 0. + +Managing O2CB +============= + +OCFS2 uses the O2CB "cluster stack" which is similar to our `xhad`. To configure +O2CB we need to + +- assign each host an integer node number (from zero) +- on pool/cluster join: update the configuration on every node to include the + new node. In OCFS2 this can be done online. +- on pool/cluster leave/eject: update the configuration on every node to exclude + the old node. In OCFS2 this needs to be done offline. + +In the current Xapi toolstack there is a single global implicit cluster called a "Pool" +which is used for: resource locking; "clustered" storage repositories and fault handling (in HA). In the long term we will allow these types of clusters to be +managed separately or all together, depending on the sophistication of the +admin and the complexity of their environment. We will take a small step in that +direction by keeping the OCFS2 O2CB cluster management code at "arms length" +from the Xapi Pool.join code. + +In +[xcp-idl](https://github.com/xapi-project/xcp-idl) +we will define a new API category called "Cluster" (in addition to the +categories for +[Xen domains](https://github.com/xapi-project/xcp-idl/blob/37c676548a53b927ac411ab51f33892a7b891fda/xen/xenops_interface.ml#L102) +, [ballooning](https://github.com/xapi-project/xcp-idl/blob/37c676548a53b927ac411ab51f33892a7b891fda/memory/memory_interface.ml#L38) +, [stats](https://github.com/xapi-project/xcp-idl/blob/37c676548a53b927ac411ab51f33892a7b891fda/rrd/rrd_interface.ml#L76) +, +[networking](https://github.com/xapi-project/xcp-idl/blob/37c676548a53b927ac411ab51f33892a7b891fda/network/network_interface.ml#L106) +and +[storage](https://github.com/xapi-project/xcp-idl/blob/37c676548a53b927ac411ab51f33892a7b891fda/storage/storage_interface.ml#L51) +). These APIs will only be called by Xapi on localhost. In particular they will +not be called across-hosts and therefore do not have to be backward compatible. +These are "cluster plugin APIs". + +We will define the following APIs: + +- `Plugin:Membership.create`: add a host to a cluster. On exit the local host cluster software + will know about the new host but it may need to be restarted before the + change takes effect + - in:`hostname:string`: the hostname of the management domain + - in:`uuid:string`: a UUID identifying the host + - in:`id:int`: the lowest available unique integer identifying the host + where an integer will never be re-used unless it is guaranteed that + all nodes have forgotten any previous state associated with it + - in:`address:string list`: a list of addresses through which the host + can be contacted + - out: Task.id +- `Plugin:Membership.destroy`: removes a named host from the cluster. On exit the local + host software will know about the change but it may need to be restarted + before it can take effect + - in:`uuid:string`: the UUID of the host to remove +- `Plugin:Cluster.query`: queries the state of the cluster + - out:`maintenance_required:bool`: true if there is some outstanding configuration + change which cannot take effect until the cluster is restarted. + - out:`hosts`: a list of all known hosts together with a state including: + whether they are known to be alive or dead; or whether they are currently + "excluded" because the cluster software needs to be restarted +- `Plugin:Cluster.start`: turn on the cluster software and let the local host join +- `Plugin:Cluster.stop`: turn off the cluster software + +Xapi will be modified to: + +- add table `Cluster` which will have columns + - `name: string`: this is the name of the Cluster plugin (TODO: use same + terminology as SM?) + - `configuration: Map(String,String)`: this will contain any cluster-global + information, overrides for default values etc. + - `enabled: Bool`: this is true when the cluster "should" be running. It + may require maintenance to synchronise changes across the hosts. + - `maintenance_required: Bool`: this is true when the cluster needs to + be placed into maintenance mode to resync its configuration +- add method `XenAPI:Cluster.enable` which sets `enabled=true` and waits for all + hosts to report `Membership.enabled=true`. +- add method `XenAPI:Cluster.disable` which sets `enabled=false` and waits for all + hosts to report `Membership.enabled=false`. +- add table `Membership` which will have columns + - `id: int`: automatically generated lowest available unique integer + starting from 0 + - `cluster: Ref(Cluster)`: the type of cluster. This will never be NULL. + - `host: Ref(host)`: the host which is a member of the cluster. This may + be NULL. + - `left: Date`: if not 1/1/1970 this means the time at which the host + left the cluster. + - `maintenance_required: Bool`: this is true when the Host believes the + cluster needs to be placed into maintenance mode. +- add field `Host.memberships: Set(Ref(Membership))` +- extend enum `vdi_type` to include `o2cb_statefile` as well as `ha_statefile` +- add method `Pool.enable_o2cb` with arguments + - in: `heartbeat_sr: Ref(SR)`: the SR to use for global heartbeats + - in: `configuration: Map(String,String)`: available for future configuration tweaks + - Like `Pool.enable_ha` this will find or create the heartbeat VDI, create the + `Cluster` entry and the `Membership` entries. All `Memberships` will have + `maintenance_required=true` reflecting the fact that the desired cluster + state is out-of-sync with the actual cluster state. +- add method `XenAPI:Membership.enable` + - in: `self:Host`: the host to modify + - in: `cluster:Cluster`: the cluster. +- add method `XenAPI:Membership.disable` + - in: `self:Host`: the host to modify + - in: `cluster:Cluster`: the cluster name. +- add a cluster monitor thread which + - watches the `Host.memberships` field and calls `Plugin:Membership.create` and + `Plugin:Membership.destroy` to keep the local cluster software up-to-date + when any host in the pool changes its configuration + - calls `Plugin:Cluster.query` after an `Plugin:Membership:create` or + `Plugin:Membership.destroy` to see whether the + SR needs maintenance + - when all hosts have a last start time later than a `Membership` + record's `left` date, deletes the `Membership`. +- modify `XenAPI:Pool.join` to resync with the master's `Host.memberships` list. +- modify `XenAPI:Pool.eject` to + - call `Membership.disable` in the cluster plugin to stop the `o2cb` service + - call `Membership.destroy` in the cluster plugin to remove every other host + from the local configuration + - remove the `Host` metadata from the pool + - set `XenAPI:Membership.left` to `NOW()` +- modify `XenAPI:Host.forget` to + - remove the `Host` metadata from the pool + - set `XenAPI:Membership.left` to `NOW()` + - set `XenAPI:Cluster.maintenance_required` to true + +A Cluster plugin called "o2cb" will be added which + +- on `Plugin:Membership.destroy` + - comment out the relevant node id in cluster.conf + - set the 'needs a restart' flag +- on `Plugin:Membership.create` + - if the provided node id is too high: return an error. This means the + cluster needs to be rebooted to free node ids. + - if the node id is not too high: rewrite the cluster.conf using + the "online" tool. +- on `Plugin:Cluster.start`: find the VDI with `type=o2cb_statefile`; + add this to the "static-vdis" list; `chkconfig` the service on. We + will use the global heartbeat mode of `o2cb`. +- on `Plugin:Cluster.stop`: stop the service; `chkconfig` the service off; + remove the "static-vdis" entry; leave the VDI itself alone +- keeps track of the current 'live' cluster.conf which allows it to + - report the cluster service as 'needing a restart' (which implies + we need maintenance mode) + +Summary of differences between this and xHA: + +- we allow for the possibility that hosts can join and leave, without + necessarily taking the whole cluster down. In the case of `o2cb` we + should be able to have `join` work live and only `eject` requires + maintenance mode +- rather than write explicit RPCs to update cluster configuration state + we instead use an event watch and resync pattern, which is hopefully + more robust to network glitches while a reconfiguration is in progress. + +Managing xhad +============= + +We need to ensure `o2cb` and `xhad` do not try to conflict by fencing +hosts at the same time. We shall: + +- use the default `o2cb` timeouts (hosts fence if no I/O in 60s): this + needs to be short because disk I/O *on otherwise working hosts* can + be blocked while another host is failing/ has failed. + +- make the `xhad` host fence timeouts much longer: 300s. It's much more + important that this is reliable than fast. We will make this change + globally and not just when using OCFS2. + +In the `xhad` config we will cap the `HeartbeatInterval` and `StatefileInterval` +at 5s (the default otherwise would be 31s). This means that 60 heartbeat +messages have to be lost before `xhad` concludes that the host has failed. + +SM plugin +========= + +The SM plugin `OCFS2` will be a file-based plugin. + +TODO: which file format by default? + +The SM plugin will first check whether the `o2cb` cluster is active and fail +operations if it is not. + +I/O paths +========= + +When either HA or OCFS O2CB "fences" the host it will look to the admin like +a host crash and reboot. We need to (in priority order) + +1. help the admin *prevent* fences by monitoring their I/O paths + and fixing issues before they lead to trouble +2. when a fence/crash does happen, help the admin + - tell the difference between an I/O error (admin to fix) and a software + bug (which should be reported) + - understand how to make their system more reliable + + +Monitoring I/O paths +-------------------- + +If heartbeat I/O fails for more than 60s when running `o2cb` then the host will fence. +This can happen either + +- for a good reason: for example the host software may have deadlocked or someone may + have pulled out a network cable. + +- for a bad reason: for example a network bond link failure may have been ignored + and then the second link failed; or the heartbeat thread may have been starved of + I/O bandwidth by other processes + +Since the consequences of fencing are severe -- all VMs on the host crash simultaneously -- +it is important to avoid the host fencing for bad reasons. + +We should recommend that all users + +- use network bonding for their network heartbeat +- use multipath for their storage heartbeat + +Furthermore we need to *help* users monitor their I/O paths. It's no good if they use +a bonded network but fail to notice when one of the paths have failed. + +The current XenServer HA implementation generates the following I/O-related alerts: + +- `HA_HEARTBEAT_APPROACHING_TIMEOUT` (priority 5 "informational"): when half the + network heartbeat timeout has been reached. +- `HA_STATEFILE_APPROACHING_TIMEOUT` (priority 5 "informational"): when half the + storage heartbeat timeout has been reached. +- `HA_NETWORK_BONDING_ERROR` (priority 3 "service degraded"): when one of the bond + links have failed. +- `HA_STATEFILE_LOST` (priority 2 "service loss imminent"): when the storage heartbeat + has completely failed and only the network heartbeat is left. +- MULTIPATH_PERIODIC_ALERT (priority 3 "service degrated"): when one of the multipath + links have failed. + +Unfortunately alerts are triggered on "edges" i.e. when state changes, and not on "levels" +so it is difficult to see whether the link is currently broken. + +We should define datasources suitable for use by xcp-rrdd to expose the current state +(and the history) of the I/O paths as follows: + +- `pif__paths_failed`: the total number of paths which we know have failed. +- `pif__paths_total`: the total number of paths which are configured. +- `sr__paths_failed`: the total number of storage paths which we know have failed. +- `sr__paths_total`: the total number of storage paths which are configured. + +The `pif` datasources should be generated by `xcp-networkd` which already has a +[network bond monitoring thread](https://github.com/xapi-project/xcp-networkd/blob/bc0140feba19cf8dcced3bd66e54eeee112af819/networkd/network_monitor_thread.ml#L52). +THe `sr` datasources should be generated by `xcp-rrdd` plugins since there is no +storage daemon to generate them. +We should create RRDs using the `MAX` consolidation function, otherwise information +about failures will be lost by averaging. + +XenCenter (and any diagnostic tools) should warn when the system is at risk of fencing +in particular if any of the following are true: + +- `pif__paths_failed` is non-zero +- `sr__paths_failed` is non-zero +- `pif__paths_total` is less than 2 +- `sr__paths_total` is less than 2 + +XenCenter (and any diagnostic tools) should warn if any of the following *have been* +true over the past 7 days: + +- `pif__paths_failed` is non-zero +- `sr__paths_failed` is non-zero + + +Heartbeat "QoS" +--------------- + +The network and storage paths used by heartbeats *must* remain responsive otherwise +the host will fence (i.e. the host and all VMs will crash). + +Outstanding issue: how slow can `multipathd` get? How does it scale with the number of +LUNs. + +Post-crash diagnostics +====================== + +When a host crashes the effect on the user is severe: all the VMs will also +crash. In cases where the host crashed for a bad reason (such as a single failure +after a configuration error) we must help the user understand how they can +avoid the same situation happening again. + +We must make sure the crash kernel runs reliably when `xhad` and `o2cb` +fence the host. + +Xcp-rrdd will be modified to store RRDs in an `mmap(2)`d file sin the dom0 +filesystem (rather than in-memory). Xcp-rrdd will call `msync(2)` every 5s +to ensure the historical records have hit the disk. We should use the same +on-disk format as RRDtool (or as close to it as makes sense) because it has +already been optimised to minimise the amount of I/O. + +Xapi will be modified to run a crash-dump analyser program `xen-crash-analyse`. + +`xen-crash-analyse` will: + +- parse the Xen and dom0 stacks and diagnose whether + - the dom0 kernel was panic'ed by `o2cb` + - the Xen watchdog was fired by `xhad` + - anything else: this would indicate a bug that should be reported +- in cases where the system was fenced by `o2cb` or `xhad` then the analyser + - will read the archived RRDs and look for recent evidence of a path failure + or of a bad configuration (i.e. one where the total number of paths is 1) + - will parse the `xhad.log` and look for evidence of heartbeats "approaching + timeout" + +TODO: depending on what information we can determine from the analyser, we +will want to record some of it in the `Host_crash_dump` database table. + +XenCenter will be modified to explain why the host crashed and explain what +the user should do to fix it, specifically: + +- if the host crashed for no obvious reason then consider this a software + bug and recommend a bugtool/system-status-report is taken and uploaded somewhere +- if the host crashed because of `o2cb` or `xhad` then either + - if there is evidence of path failures in the RRDs: recommend the user + increase the number of paths or investigate whether some of the equipment + (NICs or switches or HBAs or SANs) is unreliable + - if there is evidence of insufficient paths: recommend the user add more + paths + +Network configuration +===================== + +The documentation should strongly recommend + +- the management network is bonded +- the management network is dedicated i.e. used only for management traffic + (including heartbeats) +- the OCFS2 storage is multipathed + +`xcp-networkd` will be modified to change the behaviour of the DHCP client. +Currently the `dhclient` will wait for a response and eventually background +itself. This is a big problem since DHCP can reset the hostname, and this can +break `o2cb`. Therefore we must insist that `PIF.reconfigure_ip` becomes +fully synchronous, supporting timeout and cancellation. Once the call returns +-- whether through success or failure -- there must not be anything in the +background which will change the system's hostname. + +TODO: figure out whether we need to request "maintenance mode" for hostname +changes. + +Maintenance mode +================ + +The purpose of "maintenance mode" is to take a host out of service and leave +it in a state where it's safe to fiddle with it without affecting services +in VMs. + +XenCenter currently does the following: + +- `Host.disable`: prevents new VMs starting here +- makes a list of all the VMs running on the host +- `Host.evacuate`: move the running VMs somewhere else + +The problems with maintenance mode are: + +- it's not safe to fiddle with the host network configuration with storage + still attached. For NFS this risks deadlocking the SR. For OCFS2 this + risks fencing the host. +- it's not safe to fiddle with the storage or network configuration if HA + is running because the host will be fenced. It's not safe to disable fencing + unless we guarantee to reboot the host on exit from maintenance mode. + +We should also + +- `PBD.unplug`: all storage. This allows the network to be safely reconfigured. + If the network is configured when NFS storage is plugged then the SR can + permanently deadlock; if the network is configured when OCFS2 storage is + plugged then the host can crash. + +TODO: should we add a `Host.prepare_for_maintenance` (better name TBD) +to take care of all this without XenCenter having to script it. This would also +help CLI and powershell users do the right thing. + +TODO: should we insist that the host is rebooted to leave maintenance +mode? This would make maintenance mode more reliable and allow us to integrate +maintenance mode with xHA (where maintenance mode is a "staged reboot") + +TODO: should we leave all clusters as part of maintenance mode? We +probably need to do this to avoid fencing. + +Walk-through: adding OCFS2 storage +================================== + +Assume you have an existing Pool of 2 hosts. First the client will set up +the O2CB cluster, choosing where to put the global heartbeat volume. The +client should check that the I/O paths have all been setup correctly with +bonding and multipath and prompt the user to fix any obvious problems. + +![The client enables O2CB and then creates an SR](o2cb-enable-external.svg) + +Internally within `Pool.enable_o2cb` Xapi will set up the cluster metadata +on every host in the pool: + +![Xapi creates the cluster configuration and each host updates its metadata](o2cb-enable-internal1.svg) + +At this point all hosts have in-sync `cluster.conf` files but all cluster +services are disabled. We also have `requires_mainenance=true` on all +`Membership` entries and the global `Cluster` has `enabled=false`. +The client will now try to enable the cluster with `Cluster.enable`: + +![Xapi enables the cluster software on all hosts](o2cb-enable-internal2.svg) + +Now all hosts are in the cluster and the SR can be created using the standard +SM APIs. + +Walk-through: remove a host +=========================== + +Assume you have an existing Pool of 2 hosts with `o2cb` clustering enabled +and at least one `ocfs2` filesystem mounted. If the host is online then +`XenAPI:Pool.eject` will: + +![Xapi ejects a host from the pool](pool-eject.svg) + +Note that: + +- All hosts will have modified their `o2cb` `cluster.conf` to comment out + the former host +- The `Membership` table still remembers the node number of the ejected host-- + this cannot be re-used until the SR is taken down for maintenance. +- All hosts can see the difference between their current `cluster.conf` + and the one they would use if they restarted the cluster service, so all + hosts report that the cluster must be taken offline i.e. `requires_maintence=true`. + +Summary of the impact on the admin +================================== + +OCFS2 is fundamentally a different type of storage to all existing storage +types supported by xapi. OCFS2 relies upon O2CB, which provides +[Host-level High Availability](../../../features/HA/HA.html). All HA implementations +(including O2CB and `xhad`) impose restrictions on the server admin to +prevent unnecessary host "fencing" (i.e. crashing). Once we have OCFS2 as +a feature, we will have to live with these restrictions which previously only +applied when HA was explicitly enabled. To reduce complexity we will not try +to enforce restrictions only when OCFS2 is being used or is likely to be used. + +Impact even if not using OCFS2 +------------------------------ + +- "Maintenance mode" now includes detaching all storage. +- Host network reconfiguration can only be done in maintenance mode +- XenServer HA enable takes longer +- XenServer HA failure detection takes longer +- Network configuration with DHCP must be fully synchronous i.e. it wil block + until the DHCP server responds. On a timeout, the change will not be made. + +Impact when using OCFS2 +----------------------- + +- Sometimes a host will not be able to join the pool without taking the + pool into maintenance mode +- Every VM will have to be XSM'ed (is that a verb?) to the new OCFS2 storage. + This means that VMs with more than 2 snapshots will have their snapshots + deleted; it means you need to provision another storage target, temporarily + doubling your storage needs; and it will take a long time. +- There will now be 2 different reasons why a host has fenced which the + admin needs to understand. diff --git a/doc/content/design/ocfs2/o2cb-enable-external.msc b/doc/content/design/ocfs2/o2cb-enable-external.msc new file mode 100644 index 00000000000..9ee67403dcd --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-external.msc @@ -0,0 +1,13 @@ +Client->Xapi: Pool.enable_o2cb +Xapi->LVHD: vdi_create +LVHD-->Xapi: OK +Note left of LVHD: VDI will be used for\nO2CB global heartbeat +Xapi-->Client: OK +Note right of Xapi: DB objects initialised\nbut cluster is offline +Client->Xapi: Cluster.enable +Xapi-->Client: OK +Note right of Xapi: O2CB cluster is online\non all hosts +Client->Xapi: SR.create +Xapi->OCFS2: sr_create +OCFS2-->Xapi: OK +Xapi-->Client: OK diff --git a/doc/content/design/ocfs2/o2cb-enable-external.svg b/doc/content/design/ocfs2/o2cb-enable-external.svg new file mode 100644 index 00000000000..46832e6219c --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-external.svg @@ -0,0 +1,15 @@ +Client->Xapi: Pool.enable_o2cb +Xapi->LVHD: vdi_create +LVHD-->Xapi: OK +Note left of LVHD: VDI will be used for\nO2CB global heartbeat +Xapi-->Client: OK +Note right of Xapi: DB objects initialised\nbut cluster is offline +Client->Xapi: Cluster.enable +Xapi-->Client: OK +Note right of Xapi: O2CB cluster is online\non all hosts +Client->Xapi: SR.create +Xapi->OCFS2: sr_create +OCFS2-->Xapi: OK +Xapi-->Client: OK + +Created with Raphaël 2.1.0 \ No newline at end of file diff --git a/doc/content/design/ocfs2/o2cb-enable-internal1.msc b/doc/content/design/ocfs2/o2cb-enable-internal1.msc new file mode 100644 index 00000000000..3bcf4a4f6b6 --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-internal1.msc @@ -0,0 +1,14 @@ +Participant Plugin\nSlave +Participant Xapi\nSlave +Participant Xapi\nMaster +Xapi\nSlave->Xapi\nMaster: events.from +Note over Xapi\nMaster: Create Cluster\nMemberships in DB +Xapi\nMaster-->Xapi\nSlave: events.from OK +Xapi\nMaster->Plugin\nMaster: Membership.create +Note over Plugin\nMaster: edit cluster.conf +Xapi\nMaster->Plugin\nMaster: Cluster.query +Plugin\nMaster->Xapi\nMaster: requires_maintenance=true +Xapi\nSlave->Plugin\nSlave: Membership.create +Note over Plugin\nSlave: edit cluster.conf +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: requires_maintenance=true diff --git a/doc/content/design/ocfs2/o2cb-enable-internal1.svg b/doc/content/design/ocfs2/o2cb-enable-internal1.svg new file mode 100644 index 00000000000..b3f6ed0d571 --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-internal1.svg @@ -0,0 +1,15 @@ +Participant Plugin\nSlave +Participant Xapi\nSlave +Participant Xapi\nMaster +Xapi\nSlave->Xapi\nMaster: events.from +Note over Xapi\nMaster: Create Cluster\nMemberships in DB +Xapi\nMaster-->Xapi\nSlave: events.from OK +Xapi\nMaster->Plugin\nMaster: Membership.create +Note over Plugin\nMaster: edit cluster.conf +Xapi\nMaster->Plugin\nMaster: Cluster.query +Plugin\nMaster->Xapi\nMaster: requires_maintenance=true +Xapi\nSlave->Plugin\nSlave: Membership.create +Note over Plugin\nSlave: edit cluster.conf +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: requires_maintenance=true +Created with Raphaël 2.1.0 \ No newline at end of file diff --git a/doc/content/design/ocfs2/o2cb-enable-internal2.msc b/doc/content/design/ocfs2/o2cb-enable-internal2.msc new file mode 100644 index 00000000000..d917cd8e734 --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-internal2.msc @@ -0,0 +1,16 @@ +Participant Plugin\nMaster +Xapi\nMaster->Xapi\nSlave: Membership.enable +Xapi\nMaster->Xapi\nMaster: Membership.enable +Xapi\nSlave->Plugin\nSlave: Membership.enable +Xapi\nMaster->Plugin\nMaster: Membership.enable +Note over Plugin\nSlave: chkconfig o2cb on +Note over Plugin\nMaster: chkconfig o2cb on +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: not enabled yet +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: enabled +Note over Xapi\nSlave: requires_maintenance=false +Xapi\nMaster->Plugin\nMaster: Cluster.query +Plugin\nMaster->Xapi\nMaster: enabled +Note over Xapi\nMaster: requires_maintenance=false +Note over Xapi\nMaster: enabled=true diff --git a/doc/content/design/ocfs2/o2cb-enable-internal2.svg b/doc/content/design/ocfs2/o2cb-enable-internal2.svg new file mode 100644 index 00000000000..e8c4da29580 --- /dev/null +++ b/doc/content/design/ocfs2/o2cb-enable-internal2.svg @@ -0,0 +1,17 @@ +Participant Plugin\nMaster +Xapi\nMaster->Xapi\nSlave: Membership.enable +Xapi\nMaster->Xapi\nMaster: Membership.enable +Xapi\nSlave->Plugin\nSlave: Membership.enable +Xapi\nMaster->Plugin\nMaster: Membership.enable +Note over Plugin\nSlave: chkconfig o2cb on +Note over Plugin\nMaster: chkconfig o2cb on +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: not enabled yet +Xapi\nSlave->Plugin\nSlave: Cluster.query +Plugin\nSlave->Xapi\nSlave: enabled +Note over Xapi\nSlave: requires_maintenance=false +Xapi\nMaster->Plugin\nMaster: Cluster.query +Plugin\nMaster->Xapi\nMaster: enabled +Note over Xapi\nMaster: requires_maintenance=false +Note over Xapi\nMaster: enabled=true +Created with Raphaël 2.1.0 \ No newline at end of file diff --git a/doc/content/design/ocfs2/ocfs2.graffle b/doc/content/design/ocfs2/ocfs2.graffle new file mode 100644 index 0000000000000000000000000000000000000000..2ef614fc9191e4237cd49e7b0f602f1a60e4bc36 GIT binary patch literal 3640 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 KC~CQzp#T6idO|Az literal 0 HcmV?d00001 diff --git a/doc/content/design/ocfs2/ocfs2.png b/doc/content/design/ocfs2/ocfs2.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf25f8a574337a873cfda264c99601dec2b4bf5 GIT binary patch literal 49597 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\nSlave:Pool.eject +Xapi\nSlave->Plugin\nSlave:Membership.destroy +Xapi\nSlave->Plugin\nSlave:Membership.destroy +Xapi\nSlave->Plugin\nSlave:Cluster.disable +Note over Plugin\nSlave:chkconfig o2cb off\nreboot +Note over Xapi\nMaster:remove Host metadata +Note over Xapi\nMaster:set Membership.left to NOW() +Xapi\nMaster->Plugin\nMaster:Membership.destroy +Xapi\nMaster->Plugin\nMaster:Cluster.query +Note over Xapi\nMaster:Cluster.requires_maintenance=true + diff --git a/doc/content/design/ocfs2/pool-eject.svg b/doc/content/design/ocfs2/pool-eject.svg new file mode 100644 index 00000000000..5f57ce745ad --- /dev/null +++ b/doc/content/design/ocfs2/pool-eject.svg @@ -0,0 +1,13 @@ +Participant Plugin\nMaster +Participant Xapi\nMaster +Xapi\nMaster->Xapi\nSlave:Pool.eject +Xapi\nSlave->Plugin\nSlave:Membership.destroy +Xapi\nSlave->Plugin\nSlave:Membership.destroy +Xapi\nSlave->Plugin\nSlave:Cluster.disable +Note over Plugin\nSlave:chkconfig o2cb off\nreboot +Note over Xapi\nMaster:remove Host metadata +Note over Xapi\nMaster:set Membership.left to NOW() +Xapi\nMaster->Plugin\nMaster:Membership.destroy +Xapi\nMaster->Plugin\nMaster:Cluster.query +Note over Xapi\nMaster:Cluster.requires_maintenance=true +Created with Raphaël 2.1.0 \ No newline at end of file diff --git a/doc/content/design/patches-in-vdis.md b/doc/content/design/patches-in-vdis.md new file mode 100644 index 00000000000..13c1f9f4a68 --- /dev/null +++ b/doc/content/design/patches-in-vdis.md @@ -0,0 +1,77 @@ +--- +title: patches in VDIs +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +"Patches" are signed binary blobs which can be queried and applied. +They are stored in the dom0 filesystem under `/var/patch`. Unfortunately +the patches can be quite large -- imagine a repo full of RPMs -- and +the dom0 filesystem is usually quite small, so it can be difficult +to upload and apply some patches. + +Instead of writing patches to the dom0 filesystem, we shall write them +to disk images (VDIs) instead. We can then take advantage of features like + +- shared storage +- cross-host `VDI.copy` + +to manage the patches. + +XenAPI changes +============== + +1. Add a field `pool_patch.VDI` of type `Ref(VDI)`. When a new patch is + stored in a VDI, it will be referenced here. Older patches and cleaned + patches will have invalid references here. + +2. The HTTP handler for uploading patches will choose an SR to stream the + patch into. It will prefer to use the `pool.default_SR` and fall back + to choosing an SR on the master whose driver supports the `VDI_CLONE` + capability: we want the ability to fast clone patches, one per host + concurrently installing them. A VDI will be created whose size is 4x + the apparent size of the patch, defaulting to 4GiB if we have no size + information (i.e. no `content-length` header) + +3. `pool_patch.clean_on_host` will be deprecated. It will still try to + clean a patch *from the local filesystem* but this is pointless for + the new VDI patch uploads. + +4. `pool_patch.clean` will be deprecated. It will still try to clean a patch + from the *local filesystem* of the master but this is pointless for the + new VDI patch uploads. + +4. `pool_patch.pool_clean` will be deprecated. It will destroy any associated + patch VDI. Users will be encouraged to call `VDI.destroy` instead. + + + +Changes beneath the XenAPI +========================== + +1. `pool_patch` records will only be deleted if both the `filename` field + refers to a missing file on the master *and* the `VDI` field is a dangling + reference + +2. Patches stored in VDIs will be stored within a filesystem, like we used + to do with suspend images. This is needed because (a) we want to execute + the patches and block devices cannot be executed; and (b) we can use + spare space in the VDI as temporary scratch space during the patch + application process. Within the VDI we will call patches `patch` rather + than using a complicated filename. + +3. When a host wishes to apply a patch it will call `VDI.copy` to duplicate + the VDI to a locally-accessible SR, mount the filesystem and execute it. + If the patch is still in the master's dom0 filesystem then it will fall + back to the HTTP handler. + + +Summary of the impact on the admin +================================== + +- There will nolonger be a size limit on hotfixes imposed by the mechanism + itself. +- There must be enough free space in an SR connected to the host to be able + to apply a patch on that host. diff --git a/doc/content/design/pci-passthrough.md b/doc/content/design/pci-passthrough.md new file mode 100644 index 00000000000..c91cc8d7b02 --- /dev/null +++ b/doc/content/design/pci-passthrough.md @@ -0,0 +1,76 @@ +--- +title: PCI passthrough support +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +Introduction +------------ + +GPU passthrough is already available in XAPI, this document proposes to also +offer passthrough for all PCI devices through XAPI. + +Design proposal +--------------- + +New methods for PCI object: +- `PCI.enable_dom0_access` +- `PCI.disable_dom0_access` +- `PCI.get_dom0_access_status`: compares the outputs of `/opt/xensource/libexec/xen-cmdline` + and `/proc/cmdline` to produce one of the four values that can be currently contained + in the `PGPU.dom0_access` field: + - disabled + - disabled_on_reboot + - enabled + - enabled_on_reboot + + How do determine the expected dom0 access state: + If the device id is present in both `pciback.hide` of `/proc/cmdline` and `xen-cmdline`: `enabled` + If the device id is present not in both `pciback.hide` of `/proc/cmdline` and `xen-cmdline`: `disabled` + If the device id is present in the `pciback.hide` of `/proc/cmdline` but not in the one of `xen-cmdline`: `disabled_on_reboot` + If the device id is not present in the `pciback.hide` of `/proc/cmdline` but is in the one of `xen-cmdline`: `enabled_on_reboot` + + A function rather than a field makes the data always accurate and even accounts for + changes made by users outside XAPI, directly through `/opt/xensource/libexec/xen-cmdline` + +With these generic methods available, the following field and methods will be *deprecated*: +- `PGPU.enable_dom0_access` +- `PGPU.disable_dom0_access` +- `PGPU.dom0_access` (DB field) + +They would still be usable and up to date with the same info as for the PCI methods. + +Test cases +---------- + +- hide a PCI: + - call `PCI.disable_dom0_access` on an `enabled` PCI + - check the PCI goes in state `disabled_on_reboot` + - reboot the host + - check the PCI goes in state `disabled` + + +- unhide a PCI: + - call `PCI.enable_dom0_access` on an `disabled` PCI + - check the PCI goes in state `enabled_on_reboot` + - reboot the host + - check the PCI goes in state `enabled` + +- get a PCI dom0 access state: + - on a `enabled` PCI, make sure the `get_dom0_access_status` returns `enabled` + - hide the PCI + - make sure the `get_dom0_access_status` returns `disabled_on_reboot` + - reboot + - make sure the `get_dom0_access_status` returns `disabled` + - unhide the PCI + - make sure the `get_dom0_access_status` returns `enabled_on_reboot` + - reboot + - make sure the `get_dom0_access_status` returns `enabled` + +- Check PCI/PGPU dom0 access coherence: + - hide a PCI belonging to a PGPU and make sure both states remains coherent at every step + - unhide a PCI belonging to a PGPU and make sure both states remains coherent at every step + - hide a PGPU and make sure its and its PCI's states remains coherent at every step + - unhide a PGPU and make sure its and its PCI's states remains coherent at every step diff --git a/doc/content/design/pif-properties.md b/doc/content/design/pif-properties.md new file mode 100644 index 00000000000..f53c7efbc3e --- /dev/null +++ b/doc/content/design/pif-properties.md @@ -0,0 +1,64 @@ +--- +title: GRO and other properties of PIFs +layout: default +design_doc: true +revision: 1 +status: released (6.5) +--- + +It has been possible to enable and disable GRO and other "ethtool" features on +PIFs for a long time, but there was never an official API for it. Now there is. + +Introduction +------------ + +The former way to enable GRO via the CLI is as follows: + + xe pif-param-set uuid= other-config:ethtool-gro=on + xe pif-plug uuid= + +The `other-config` field is a grab-bag of options that are not clearly defined. +The options exposed through `other-config` are mostly experimental features, and +the interface is not considered stable. Furthermore, the field is read/write +and does not have any input validation, and cannot not trigger any actions +immediately. The latter is why it is needed to call `pif-plug` after setting +the `ethtool-gro` key, in order to actually make things happen. + +New API +------- + +New field: + +* Field `PIF.properties` of type `(string -> string) map`. +* Physical and bond PIFs have a `gro` key in their `properties`, with possible values `on` and `off`. There are currently no other properties defined. +* VLAN and Tunnel PIFs do not have any properties. They implicitly inherit the properties from the PIF they are based upon (either a physical PIF or a bond). +* For backwards compatibility, if there is a `other-config:ethtool-gro` key present on the PIF, it will be treated as an override of the `gro` key in `PIF.properties`. + +New function: + +* Message `void PIF.set_property (PIF ref, string, string)`. + * First argument: the reference of the PIF to act on. + * Second argument: the key to change in the `properties` field. + * Third argument: the value to write. +* The function can only be used on physical PIFs that are not bonded, and on bond PIFs. Attempts to call the function on bond slaves, VLAN PIFs, or Tunnel PIFs, fail with `CANNOT_CHANGE_PIF_PROPERTIES`. +* Calls with invalid keys or values fail with `INVALID_VALUE`. +* When called on a bond PIF, the key in the `properties` of the associated bond slaves will also be set to same value. +* The function automatically causes the settings to be applied to the network devices (no additional `plug` is needed). This includes any VLANs that are on top of the PIF to-be-changed, as well as any bond slaves. + +Defaults, Installation and Upgrade +------------------------ + +* Any newly introduced PIF will have its `properties` field set to `"gro" -> "on"`. This includes PIFs obtained after a fresh installation of XenServer, as well as PIFs created using `PIF.introduce` or `PIF.scan`. In other words, GRO will be "on" by default. +* An upgrade from a version of XenServer that does not have the `PIF.properties` field, will give every physical and bond PIF a `properties` field set to `"gro" -> "on"`. In other words, GRO will be "on" by default after an upgrade. + +Bonding +------- + +* When creating a bond, the bond-slaves-to-be must all have equal `PIF.properties`. If not, the `bond.create` call will fail with `INCOMPATIBLE_BOND_PROPERTIES`. +* When a bond is created successfully, the `properties` of the bond PIF will be equal to the properties of the bond slaves. + +Command Line Interface +---------------------- + +* The `PIF.properties` field is exposed through `xe pif-list` and `xe pif-param-list` as usual. +* The `PIF.set_property` call is exposed through `xe pif-param-set`. For example: `xe pif-param-set uuid= properties:gro=off`. diff --git a/doc/content/design/plugin-protocol-v2.md b/doc/content/design/plugin-protocol-v2.md new file mode 100644 index 00000000000..8c02b85c61f --- /dev/null +++ b/doc/content/design/plugin-protocol-v2.md @@ -0,0 +1,198 @@ +--- +title: RRDD plugin protocol v2 +layout: default +design_doc: true +revision: 1 +status: released (7.0) +revision_history: +- revision_number: 1 + description: Initial version +--- + +Motivation +---------- + +rrdd plugins currently report datasources via a shared-memory file, using the +following format: + +``` +DATASOURCES +000001e4 +dba4bf7a84b6d11d565d19ef91f7906e +{ + "timestamp": 1339685573, + "data_sources": { + "cpu-temp-cpu0": { + "description": "Temperature of CPU 0", + "type": "absolute", + "units": "degC", + "value": "64.33" + "value_type": "float", + }, + "cpu-temp-cpu1": { + "description": "Temperature of CPU 1", + "type": "absolute", + "units": "degC", + "value": "62.14" + "value_type": "float", + } + } +} +``` + +This format contains four main components: + +* A constant header string + +`DATASOURCES` + +This should always be present. + +* The JSON data length, encoded as hexadecimal + +`000001e4` + +* The md5sum of the JSON data + +`dba4bf7a84b6d11d565d19ef91f7906e` + +* The JSON data itself, encoding the values and metadata associated with the +reported datasources. + +### Example +``` +{ + "timestamp": 1339685573, + "data_sources": { + "cpu-temp-cpu0": { + "description": "Temperature of CPU 0", + "type": "absolute", + "units": "degC", + "value": "64.33" + "value_type": "float", + }, + "cpu-temp-cpu1": { + "description": "Temperature of CPU 1", + "type": "absolute", + "units": "degC", + "value": "62.14" + "value_type": "float", + } + } +} +``` + +The disadvantage of this protocol is that rrdd has to parse the entire JSON +structure each tick, even though most of the time only the values will change. + +For this reason a new protocol is proposed. + +Protocol V2 +----------- + +|value|bits|format|notes| +|-----|----|------|-----| +|header string |(string length)*8|string|"DATASOURCES" as in the V1 protocol | +|data checksum |32 |int32 |binary-encoded crc32 of the concatenation of the encoded timestamp and datasource values| +|metadata checksum |32 |int32 |binary-encoded crc32 of the metadata string (see below) | +|number of datasources|32 |int32 |only needed if the metadata has changed - otherwise RRDD can use a cached value | +|timestamp |64 |int64 |Unix epoch | +|datasource values |n * 64 |int64 \| double |n is the number of datasources exported by the plugin, type dependent on the setting in the metadata for value_type [int64\|float] | +|metadata length |32 |int32 | | +|metadata |(string length)*8|string| | + +All integers/double are bigendian. The metadata will have the same JSON-based format as +in the V1 protocol, minus the timestamp and `value` key-value pair for each +datasource. + +| field | values | notes | required | +|-------|--------|-------|----------| +|description|string|Description of the datasource|no| +|owner|host \| vm \| sr|The object to which the data relates|no, default host| +|value_type|int64 \| float|The type of the datasource|yes| +|type|absolute \| derive \| gauge|The type of measurement being sent. Absolute for counters which are reset on reading, derive stores the derivative of the recorded values (useful for metrics which continually increase like amount of data written since start), gauge for things like temperature|no, default absolute| +|default|true \| false|Whether the source is default enabled or not|no, default false| +|units||The units the data should be displayed in|no| +|min||The minimum value for the datasource|no, default -infinity| +|max||The maximum value for the datasource|no, default +infinity| + + +### Example +``` +{ + "datasources": { + "memory_reclaimed": { + "description":"Host memory reclaimed by squeezed", + "owner":"host", + "value_type":"int64", + "type":"absolute", + "default":"true", + "units":"B", + "min":"-inf", + "max":"inf" + }, + "memory_reclaimed_max": { + "description":"Host memory that could be reclaimed by squeezed", + "owner":"host", + "value_type":"int64", + "type":"absolute", + "default":"true", + "units":"B", + "min":"-inf", + "max":"inf" + }, + { + "cpu-temp-cpu0": { + "description": "Temperature of CPU 0", + "owner":"host", + "value_type": "float", + "type": "absolute", + "default":"true", + "units": "degC", + "min":"-inf", + "max":"inf" + }, + "cpu-temp-cpu1": { + "description": "Temperature of CPU 1", + "owner":"host", + "value_type": "float", + "type": "absolute", + "default":"true", + "units": "degC", + "min":"-inf", + "max":"inf" + } + } +} +``` + +The above formatting is not required, but added here for readability. + +Reading algorithm +----------------- + +``` +if header != expected_header: + raise InvalidHeader() +if data_checksum == last_data_checksum: + raise NoUpdate() +if data_checksum != crc32(encoded_timestamp_and_values): + raise InvalidChecksum() +if metadata_checksum == last_metadata_checksum: + for datasource, value in cached_datasources, values: + update(datasource, value) +else: + if metadata_checksum != crc32(metadata): + raise InvalidChecksum() + cached_datasources = create_datasources(metadata) + for datasource, value in cached_datasources, values: + update(datasource, value) +``` + +This means that for a normal update, RRDD will only have to read the header plus +the first (16 + 16 + 4 + 8 + 8*n) bytes of data, where n is the number of +datasources exported by the plugin. If the metadata changes RRDD will have to +read all the data (and parse the metadata). + +n.b. the timestamp reported by plugins is not currently used by RRDD - it uses +its own global timestamp. diff --git a/doc/content/design/plugin-protocol-v3.md b/doc/content/design/plugin-protocol-v3.md new file mode 100644 index 00000000000..27596b63488 --- /dev/null +++ b/doc/content/design/plugin-protocol-v3.md @@ -0,0 +1,81 @@ +--- +title: RRDD plugin protocol v3 +layout: default +design_doc: true +revision: 1 +status: proposed +revision_history: +- revision_number: 1 + description: Initial version +--- + +Motivation +---------- + +rrdd plugins protocol v2 report datasources via shared-memory file, however it +has various limitations : + - metrics are unique by their names, thus it is not possible cannot have + several metrics that shares a same name (e.g vCPU usage per vm) + - only number metrics are supported, for example we can't expose string + metrics (e.g CPU Model) + +Therefore, it implies various limitations on plugins and limits +[OpenMetrics](https://openmetrics.io/) support for the metrics daemon. + +Moreover, it may not be practical for plugin developpers and parser implementations : + - json implementations may not keep insersion order on maps, which can cause + issues to expose datasource values as it is sensitive to the order of the metadata map + - header length is not constant and depends on datasource count, which complicates parsing + - it still requires a quite advanced parser to convert between bytes and numbers according to metadata + +A simpler protocol is proposed, based on OpenMetrics binary format to ease plugin and parser implementations. + +Protocol V3 +----------- + +For this protocol, we still use a shared-memory file, but significantly change the structure of the file. + +| value | bits | format | notes +| -------------- | ------------------ | ------ | ------------------------------------------------------------ +| header string | 12*8=96 | string | "OPENMETRICS1" which is one byte longer than "DATASOURCES", intentionally made at 12 bytes for alignment purposes +| data checksum | 32 | uint32 | Checksum of the concatenation of the rest of the header (from timestamp) and the payload data +| timestamp | 64 | uint64 | Unix epoch +| payload length | 32 | uint32 | Payload length +| payload data | 8*(payload length) | binary | OpenMetrics encoded metrics data (protocol-buffers format) + +All values are big-endian. + +The header size is constant (28 bytes) that implementation can rely on (read +the entire header in one go, simplify usage of memory mapping). + +As opposed to protocol v2 but alike protocol v1, metadata is included along +metrics in OpenMetrics format. + +`owner` attribute for metric should be exposed using a OpenMetrics label instead (named `owner`). + +Multiple metrics that shares the same name should be exposed under the same +Metric Family and be differenciated by labels (e.g `owner`). + +Reading algorithm +----------------- + +```python +if header != expected_header: + raise InvalidHeader() +if data_checksum == last_data_checksum: + raise NoUpdate() +if timestamp == last_timestamp: + raise NoUpdate() +if data_checksum != crc32(concat_header_end_payload): + raise InvalidChecksum() + +metrics = parse_openmetrics(payload_data) + +for family in metrics: + if family_exists(family): + update_family(family) + else + create_family(family) + +track_removed_families(metrics) +``` \ No newline at end of file diff --git a/doc/content/design/pool-wide-ssh.md b/doc/content/design/pool-wide-ssh.md new file mode 100644 index 00000000000..a903c545fab --- /dev/null +++ b/doc/content/design/pool-wide-ssh.md @@ -0,0 +1,80 @@ +--- +title: Pool-wide SSH +layout: default +design_doc: true +revision: 1 +status: proposed +--- + +## Background + +The SMAPIv3 plugin architecture requires that storage plugins are able to work +in the absence of xapi. Amongst other benefits, this allows them to be tested +in isolation, are able to be shared more widely than just within the XenServer +community and will cause less load on xapi's database. + +However, many of the currently existing SMAPIv1 backends require inter-host +operations to be performed. This is achieved via the use of the Xen-API call +'host.call_plugin', which allows an API user to execute a pre-installed plugin +on any pool member. This is important for operations such as coalesce / snapshot +where the active data path for a VM somewhere in the pool needs to be refreshed +in order to complete the operation. In order to use this, the RPM in which the +SM backend lives is used to deliver a plugin script into /etc/xapi.d/plugins, +and this executes the required function when the API call is made. + +In order to support these use-cases without xapi running, a new mechanism needs +to be provided to allow the execution of required functionality on remote hosts. +The canonical method for remotely executing scripts is ssh - the secure shell. +This design proposal is setting out how xapi might manage the public and +private keys to enable passwordless authentication of ssh sessions between all +hosts in a pool. + +## Modifications to the host + +On firstboot (and after being ejected), the host should generate a +host key (already done I believe), and an authentication key for the +user (root/xapi?). + +## Modifications to xapi + +Three new fields will be added to the host object: + +- ```host.ssh_public_host_key : string```: This is the host key that identifies the host +during the initial ssh key exchange protocol. This should be added to the +'known_hosts' field of any other host wishing to ssh to this host. + +- ```host.ssh_public_authentication_key : string```: This field is the public + key used for authentication when sshing from the root account on that host - + host A. This can be added to host B's ```authorized_keys``` file in order to + allow passwordless logins from host A to host B. + +- ```host.ssh_ready : bool```: A boolean flag indicating that the configuration + files in use by the ssh server/client on the host are up to date. + +One new field will be added to the pool record: + +- ```pool.revoked_authentication_keys : string list```: This field records all +authentication keys that have been used by hosts in the past. It is updated +when a host is ejected from the pool. + +### Pool Join + +On pool join, the master creates the record for the new host and populates the +two public key fields with values supplied by the joining host. It then sets +the ```ssh_ready``` field on all other hosts to ```false```. + +On each host in the pool, a thread is watching for updates to the +```ssh_ready``` value for the local host. When this is set to false, the host +then adds the keys from xapi's database to the appropriate places in the ssh +configuration files and restarts sshd. Once this is done, the host sets the +```ssh_ready``` field to 'true' + +### Pool Eject + +On pool eject, the host's ssh_public_host_key is lost, but the authetication key is added to a list of revoked keys on the pool object. This allows all other hosts to remove the key from the authorized_keys list when they next sync, which in the usual case is immediately the database is modified due to the event watch thread. If the host is offline though, the authorized_keys file will be updated the next time the host comes online. + + +## Questions + +- Do we want a new user? e.g. 'xapi' - how would we then use this user to execute privileged things? setuid binaries? +- Is keeping the revoked_keys list useful? If we 'control the world' of the authorized_keys file, we could just remove anything that's currently in there that xapi doesn't know about diff --git a/doc/content/design/schedule-snapshot.md b/doc/content/design/schedule-snapshot.md new file mode 100644 index 00000000000..75c8348d91f --- /dev/null +++ b/doc/content/design/schedule-snapshot.md @@ -0,0 +1,89 @@ +--- +title: Schedule Snapshot Design +layout: default +design_doc: true +design_review: 186 +revision: 2 +status: proposed +revision_history: +- revision_number: 1 + description: Initial version +- revision_number: 2 + description: Renaming VMSS fields and APIs. API message_create superseeds vmss_create_alerts. +- revision_number: 3 + description: Remove VMSS alarm_config details and use existing pool wide alarm config +- revision_number: 4 + description: Renaming field from retention-value to retained-snapshots and schedule-snapshot to scheduled-snapshot +- revision_number: 5 + description: Add new API task_set_status +--- + +The scheduled snapshot feature will utilize the existing architecture of VMPR. In terms of functionality, scheduled snapshot is basically VMPR without its archiving capability. + +Introduction +------------ + +* Schedule snapshot will be a new object in xapi as VMSS. +* A pool can have multiple VMSS. +* Multiple VMs can be a part of VMSS but a VM cannot be a part of multiple VMSS. +* A VMSS takes VMs snapshot with type [`snapshot`, `checkpoint`, `snapshot_with_quiesce`]. +* VMSS takes snapshot of VMs on configured intervals: + * `hourly` -> On everyday, Each hour, Mins [0;15;30;45] + * `daily` -> On everyday, Hour [0 to 23], Mins [0;15;30;45] + * `weekly` -> Days [`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`,`Sunday`], Hour[0 to 23], Mins [0;15;30;45] +* VMSS will have a limit on retaining number of VM snapshots in range [1 to 10]. + +Datapath Design +--------------- + +* There will be a cron job for VMSS. +* VMSS plugin will go through all the scheduled snapshot policies in the pool and check if any of them are due. +* If a snapshot is due then : Go through all the VM objects in XAPI associated with this scheduled snapshot policy and create a new snapshot. +* If the snapshot operation fails, create a notification alert for the event and move to the next VM. +* Check if an older snapshot now needs to be deleted to comply with the retained snapshots defined in the scheduled policy. +* If we need to delete any existing snapshots, delete the oldest snapshot created via scheduled policy. +* Set the last-run timestamp in the scheduled policy. + +Xapi Changes +------------ + +There is a new record for VM Scheduled Snapshot with new fields. + +New fields: + +* `name-label` type `String` : Name label for VMSS. +* `name-description` type `String` : Name description for VMSS. +* `enabled` type `Bool` : Enable/Disable VMSS to take snapshot. +* `type` type `Enum` [`snapshot`; `checkpoint`; `snapshot_with_quiesce`] : Type of snapshot VMSS takes. +* `retained-snapshots` type `Int64` : Number of snapshots limit for a VM, max limit is 10 and default is 7. +* `frequency` type `Enum` [`hourly`; `daily`; `weekly`] : Frequency of taking snapshot of VMs. +* `schedule` type `Map(String,String)` with (key, value) pair: + * hour : 0 to 23 + * min : [0;15;30;45] + * days : [`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`,`Sunday`] +* `last-run-time` type Date : DateTime of last execution of VMSS. +* `VMs` type VM refs : List of VMs part of VMSS. + +New fields to VM record: + +* `scheduled-snapshot` type VMSS ref : VM part of VMSS. +* `is-vmss-snapshot` type Bool : If snapshot created from VMSS. + +New APIs +-------- + +* vmss_snapshot_now (Ref vmss, Pool_Operater) -> String : This call executes the scheduled snapshot immediately. +* vmss_set_retained_snapshots (Ref vmss, Int value, Pool_Operater) -> unit : Set the value of vmss retained snapshots, max is 10. +* vmss_set_frequency (Ref vmss, String "value", Pool_Operater) -> unit : Set the value of the vmss frequency field. +* vmss_set_type (Ref vmss, String "value", Pool_Operater) -> unit : Set the snapshot type of the vmss type field. +* vmss_set_scheduled (Ref vmss, Map(String,String) "value", Pool_Operater) -> unit : Set the vmss scheduled to take snapshot. +* vmss_add_to_schedule (Ref vmss, String "key", String "value", Pool_Operater) -> unit : Add key value pair to VMSS schedule. +* vmss_remove_from_schedule (Ref vmss, String "key", Pool_Operater) -> unit : Remove key from VMSS schedule. +* vmss_set_last_run_time (Ref vmss, DateTime "value", Local_Root) -> unit : Set the last run time for VMSS. +* task_set_status (Ref task, status_type "value", READ_ONLY) -> unit : Set the status of task owned by same user, Pool_Operator can set status for any tasks. + +New CLIs +-------- + +* vmss-create (required : "name-label";"type";"frequency", optional : "name-description";"enabled";"schedule:";"retained-snapshots") -> unit : Creates VM scheduled snapshot. +* vmss-destroy (required : uuid) -> unit : Destroys a VM scheduled snapshot. diff --git a/doc/content/design/smapiv3/index.md b/doc/content/design/smapiv3/index.md new file mode 100644 index 00000000000..219e0c1bcea --- /dev/null +++ b/doc/content/design/smapiv3/index.md @@ -0,0 +1,95 @@ +--- +title: SMAPIv3 +layout: default +design_doc: true +revision: 1 +status: released (7.6) +--- + +Xapi accesses storage through "plugins" which currently use a protocol +called "SMAPIv1". This protocol has a number of problems: + +1. the protocol has many missing features, and this leads to people + using the XenAPI from within a plugin, which is racy, difficult to + get right, unscalable and makes component testing impossible. + +2. the protocol expects plugin authors to have a deep knowledge of the + Xen storage datapath (`tapdisk`, `blkback` etc) *and* the storage. + +3. the protocol is undocumented. + +We shall create a new revision of the protocol ("SMAPIv3") to address these +problems. + +The following diagram shows the new control plane: + +![Storage control plane](smapiv3.png) + +Requests from xapi are filtered through the existing `storage_access` +layer which is responsible for managing the mapping between VM VBDs and +VDIs. + +Each plugin is represented by a named queue, with APIs for + +- querying the state of each queue +- explicitly cancelling or replying to messages + +Legacy SMAPIv1 plugins will be processed via the existing `storage_access.SMAPIv1` +module. Newer SMAPIv3 plugins will be handled by a new `xapi-storage-script` +service. + +The SMAPIv3 APIs will be defined in an IDL format in a separate repo. + +xapi-storage-script +=================== + +The `xapi-storage-script` will run as a service and will + +- use `inotify` to monitor a well-known path in dom0 +- when a directory is created, check whether it contains storage plugins by + executing a `Plugin.query` +- assuming the directory contains plugins, it will register the queue name + and start listening for messages +- when messages from `xapi` or the CLI are received, it will generate the SMAPIv3 + .json message and fork the relevant script. + +SMAPIv3 IDL +=========== + +The IDL will support + +- documentation for all functions, parameters and results + - this will be extended to be a XenAPI-style versioning scheme in future +- generating hyperlinked HTML documentation, published on github +- generating libraries for python and OCaml + - the libraries will include marshalling, unmarshalling, type-checking + and command-line parsing and help generation + +Diagnostic tools +================ + +It will be possible to view the contents of the queue associated with any +plugin, and see whether + +- the queue is being served or not (perhaps the `xapi-storage-script` has + crashed) +- there are unanswered messages (perhaps one of the messages has caused + a deadlock in the implementation?) + +It will be possible to + +- delete/clear queues/messages +- download a message-sequence chart of the last N messages for inclusion in + bugtools. + +Anatomy of a plugin +=================== + +The following diagram shows what a plugin would look like: + +![Anatomy of a plugin](plugin.png) + +The SMAPIv3 +=========== + +Please read [the current SMAPIv3 documentation](https://xapi-project.github.io/xapi-storage). diff --git a/doc/content/design/smapiv3/plugin.graffle b/doc/content/design/smapiv3/plugin.graffle new file mode 100644 index 0000000000000000000000000000000000000000..4887d51f86a268b9671a0a83635602a5ffdec727 GIT binary patch literal 3429 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 literal 0 HcmV?d00001 diff --git a/doc/content/design/smapiv3/plugin.png b/doc/content/design/smapiv3/plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..6fe3857e6f0e10f3e12fce047e03635a297cb668 GIT binary patch literal 56039 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 literal 0 HcmV?d00001 diff --git a/doc/content/design/smapiv3/smapiv3.graffle b/doc/content/design/smapiv3/smapiv3.graffle new file mode 100644 index 0000000000000000000000000000000000000000..ef59a96d5253f73bc1bbd6c963652285cdf5e49f GIT binary patch literal 5269 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

dzxROFglA0|cy-eKe`Fe8=8uw%b28x8QZxy}9+- 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/sr_rrd?session_id=&uuid= + +RRD updates are handled via a single handler for the host, VM and SR UUIDs +RRD updates for the host, VMs and SRs are handled by a a single handler at +`/rrd_updates`. Exactly what is returned will be determined by the parameters +passed to this handler. + +Whether the host RRD updates are returned is governed by the presence of +`host=true` in the parameters. `host=` or the absence of the +`host` key will mean the host RRD is not returned. + +Whether the VM RRD updates are returned is governed by the `vm_uuid` key in the +URL parameters. `vm_uuid=all` will return RRD updates for all VM RRDs. +`vm_uuid=xxx` will return the RRD updates for the VM with uuid `xxx` only. +If `vm_uuid` is `none` (or any other string which is not a valid VM UUID) then +the handler will return no VM RRD updates. If the `vm_uuid` key is absent, RRD +updates for all VMs will be returned. + +Whether the SR RRD updates are returned is governed by the `sr_uuid` key in the +URL parameters. `sr_uuid=all` will return RRD updates for all SR RRDs. +`sr_uuid=xxx` will return the RRD updates for the SR with uuid `xxx` only. +If `sr_uuid` is `none` (or any other string which is not a valid SR UUID) then +the handler will return no SR RRD updates. If the `sr_uuid` key is absent, no +SR RRD updates will be returned. + +It will be possible to mix and match these parameters; for example to return +RRD updates for the host and all VMs, the URL to use would be: + + http:///rrd_updates?session_id=&start=10258122541&host=true&vm_uuid=all&sr_uuid=none + +Or, to return RRD updates for all SRs but nothing else, the URL to use would be: + + http:///rrd_updates?session_id=&start=10258122541&host=false&vm_uuid=none&sr_uuid=all + +While behaviour is defined if any of the keys `host`, `vm_uuid` and `sr_uuid` is +missing, this is for backwards compatibility and it is recommended that clients +specify each parameter explicitly. + +## Database updating. + +If the SR is presenting a data source called 'physical_utilisation', +xapi will record this periodically in its database. In order to do +this, xapi will fork a thread that, every n minutes (2 suggested, but +open to suggestions here), will query the attached SRs, then query +RRDD for the latest data source for these, and update the database. + +The utilisation of VDIs will _not_ be updated in this way until +scalability worries for RRDs are addressed. + +Xapi will cache whether it is SR master for every attached SR and only +attempt to update if it is the SR master. + +## New APIs. + +#### xcp-rrdd: + +* Get the filesystem location where sr rrds are archived: `val sr_rrds_path : uid:string -> string` + +* Archive the sr rrds to the filesystem: `val archive_sr_rrd : sr_uuid:string -> unit` + +* Load the sr rrds from the filesystem: `val push_sr_rrd : sr_uuid:string -> unit` diff --git a/doc/content/design/thin-lvhd/allocation-plane.graffle b/doc/content/design/thin-lvhd/allocation-plane.graffle new file mode 100644 index 0000000000000000000000000000000000000000..4d1b7465bdf4b00556ce2aae63a52ae95c014662 GIT binary patch literal 5149 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 LSceJ}4gLTC1f;%{ literal 0 HcmV?d00001 diff --git a/doc/content/design/thin-lvhd/allocation-plane.png b/doc/content/design/thin-lvhd/allocation-plane.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2bd55855f96408a35f354a31b6ff1b461c0b23 GIT binary patch literal 84034 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 literal 0 HcmV?d00001 diff --git a/doc/content/design/thin-lvhd/control-plane.graffle b/doc/content/design/thin-lvhd/control-plane.graffle new file mode 100644 index 0000000000000000000000000000000000000000..b64b79a2ff10b76c3703226908745f5a7f709abf GIT binary patch literal 4422 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 M0}Gf?Ey||=08Md@bN~PV literal 0 HcmV?d00001 diff --git a/doc/content/design/thin-lvhd/control-plane.png b/doc/content/design/thin-lvhd/control-plane.png new file mode 100644 index 0000000000000000000000000000000000000000..289b3a324417d4172a32689b18ddbc728162b0d3 GIT binary patch literal 67359 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 H^5*{lCHUFc literal 0 HcmV?d00001 diff --git a/doc/content/design/thin-lvhd/index.md b/doc/content/design/thin-lvhd/index.md new file mode 100644 index 00000000000..1bf209ca266 --- /dev/null +++ b/doc/content/design/thin-lvhd/index.md @@ -0,0 +1,878 @@ +--- +title: thin LVHD storage +layout: default +design_doc: true +revision: 3 +status: proposed +--- + +LVHD is a block-based storage system built on top of Xapi and LVM. LVHD +disks are represented as LVM LVs with vhd-format data inside. When a +disk is snapshotted, the LVM LV is "deflated" to the minimum-possible +size, just big enough to store the current vhd data. All other disks are +stored "inflated" i.e. consuming the maximum amount of storage space. +This proposal describes how we could add dynamic thin-provisioning to +LVHD such that + +- disks only consume the space they need (plus an adjustable small + overhead) +- when a disk needs more space, the allocation can be done *locally* + in the common-case; in particular there is no network RPC needed +- when the resource pool master host has failed, allocations can still + continue, up to some limit, allowing time for the master host to be + recovered; in particular there is no need for very low HA timeouts. +- we can (in future) support in-kernel block allocation through the + device mapper dm-thin target. + +The following diagram shows the "Allocation plane": + +![Allocation plane](allocation-plane.png) + +All VM disk writes are channelled through `tapdisk` which keeps track +of the remaining reserved space within the device mapper device. When +the free space drops below a "low-water mark", tapdisk sends a message +to a local per-SR daemon called `local-allocator` and requests more +space. + +The `local-allocator` maintains a free pool of blocks available for +allocation locally (hence the name). It will pick some blocks and +transactionally send the update to the `xenvmd` process running +on the SRmaster via the shared ring (labelled `ToLVM queue` in the diagram) +and update the device mapper tables locally. + +There is one `xenvmd` process per SR on the SRmaster. `xenvmd` receives +local allocations from all the host shared rings (labelled `ToLVM queue` +in the diagram) and combines them together, appending them to a redo-log +also on shared storage. When `xenvmd` notices that a host's free space +(represented in the metadata as another LV) is low it allocates new free blocks +and pushes these to the host via another shared ring (labelled `FromLVM queue` +in the diagram). + +The `xenvmd` process maintains a cache of the current VG metadata for +fast query and update. All updates are appended to the redo-log to ensure +they operate in O(1) time. The redo log updates are periodically flushed +to the primary LVM metadata. + +Since the operations are stored in the redo-log and will only be removed +after the real metadata has been written, the implication is that it is +possible for the operations to be performed more than once. This will +occur if the xenvmd process exits between flushing to the real metadata +and acknowledging the operations as completed. For this to work as expected, +every individual operation stored in the redo-log _must_ be idempotent. + +Note on running out of blocks +----------------------------- + +Note that, while the host has plenty of free blocks, local allocations should +be fast. If the master fails and the local free pool starts running out +and `tapdisk` asks for more blocks, then the local allocator won't be able +to provide them. +`tapdisk` should start to slow +I/O in order to provide the local allocator more time. +Eventually if ```tapdisk``` runs +out of space before the local allocator can satisfy the request then +guest I/O will block. Note Windows VMs will start to crash if guest +I/O blocks for more than 70s. Linux VMs, no matter PV or HVM, may suffer +from "block for more than 120 seconds" issue due to slow I/O. This +known issue is that, slow I/O during dirty pages writeback/flush may +cause memory starvation, then other userland process or kernel threads +would be blocked. + +The following diagram shows the control-plane: + +![control plane](control-plane.png) + +When thin-provisioning is enabled we will be modifying the LVM metadata at +an increased rate. We will cache the current metadata in the `xenvmd` process +and funnel all queries through it, rather than "peeking" at the metadata +on-disk. Note it will still be possible to peek at the on-disk metadata but it +will be out-of-date. Peeking can still be used to query the PV state of the volume +group. + +The `xenvm` CLI uses a simple +RPC interface to query the `xenvmd` process, tunnelled through `xapi` over +the management network. The RPC interface can be used for + +- activating volumes locally: `xenvm` will query the LV segments and program + device mapper +- deactivating volumes locally +- listing LVs, PVs etc + +Note that current LVHD requires the management network for these control-plane +functions. + +When the SM backend wishes to query or update volume group metadata it should use the +`xenvm` CLI while thin-provisioning is enabled. + +The `xenvmd` process shall use a redo-log to ensure that metadata updates are +persisted in constant time and flushed lazily to the regular metadata area. + +Tunnelling through xapi will be done by POSTing to the localhost URI + + /services/xenvmd/ + +Xapi will the either proxy the request transparently to the SRmaster, or issue an +http level redirect that the xenvm CLI would need to follow. + +If the xenvmd process is not running on the host on which it should +be, xapi will start it. + + +Components: roles and responsibilities +====================================== + +`xenvmd`: + +- one per plugged SRmaster PBD +- owns the LVM metadata +- provides a fast query/update API so we can (for example) create lots of LVs very fast +- allocates free blocks to hosts when they are running low +- receives block allocations from hosts and incorporates them in the LVM metadata +- can safely flush all updates and downgrade to regular LVM + +`xenvm`: + +- a CLI which talks the `xenvmd` protocol to query / update LVs +- can be run on any host, calls (except "format" and "upgrade") are forwarded by `xapi` +- can "format" a LUN to prepare it for `xenvmd` +- can "upgrade" a LUN to prepare it for `xenvmd` + +`local_allocator`: + +- one per plugged PBD +- exposes a simple interface to `tapdisk` for requesting more space +- receives free block allocations via a queue on the shared disk from `xenvmd` +- sends block allocations to `xenvmd` and updates the device mapper target locally + +`tapdisk`: + +- monitors the free space inside LVs and requests more space when running out +- slows down I/O when nearly out of space + +`xapi`: + +- provides authenticated communication tunnels +- ensures the xenvmd daemons are only running on the correct hosts. + +`SM`: + +- writes the configuration file for xenvmd (though doesn't start it) +- has an on/off switch for thin-provisioning +- can use either normal LVM or the `xenvm` CLI + +`membership_monitor` + +- configures and manages the connections between `xenvmd` and the `local_allocator` + +Queues on the shared disk +========================= + +The `local_allocator` communicates with `xenvmd` via a pair +of queues on the shared disk. Using the disk rather than the network means +that VMs will continue to run even if the management network is not working. +In particular + +- if the (management) network fails, VMs continue to run on SAN storage +- if a host changes IP address, nothing needs to be reconfigured +- if xapi fails, VMs continue to run. + +Logical messages in the queues +------------------------------ + +The `local_allocator` needs to tell the `xenvmd` which blocks have +been allocated to which guest LV. `xenvmd` needs to tell the +`local_allocator` which blocks have become free. Since we are based on +LVM, a "block" is an extent, and an "allocation" is a segment i.e. the +placing of a physical extent at a logical extent in the logical volume. + +The `local_allocator` needs to send a message with logical contents: + +- `volume`: a human-readable name of the LV +- `segments`: a list of LVM segments which says + "place physical extent x at logical extent y using a linear mapping". + +Note this message is idempotent. + +The `xenvmd` needs to send a message with logical contents: + +- `extents`: a list of physical extents which are free for the host to use + +Although +for internal housekeeping `xenvmd` will want to assign these +physical extents to logical extents within the host's free LV, the +`local_allocator` +doesn't need to know the logical extents. It only needs to know +the set of blocks which it is free to allocate. + +Starting up the local_allocator +------------------------------- + +What happens when a `local_allocator` (re)starts, after a + +- process crash, respawn +- host crash, reboot? + +When the `local_allocator` starts up, there are 2 cases: + +1. the host has just rebooted, there are no attached disks and no running VMs +2. the process has just crashed, there are attached disks and running VMs + +Case 1 is uninteresting. In case 2 there may have been an allocation in +progress when the process crashed and this must be completed. Therefore +the operation is journalled in a local filesystem in a directory which +is deliberately deleted on host reboot (Case 1). The allocation operation +consists of: + +1. `push`ing the allocation to `xenvmd` on the SRmaster +2. updating the device mapper + +Note that both parts of the allocation operation are idempotent and hence +the whole operation is idempotent. The journalling will guarantee it executes +at-least-once. + +When the `local_allocator` starts up it needs to discover the list of +free blocks. Rather than have 2 code paths, it's best to treat everything +as if it is a cold start (i.e. no local caches already populated) and to +ask the master to resync the free block list. The resync is performed by +executing a "suspend" and "resume" of the free block queue, and requiring +the remote allocator to: + +- `pop` all block allocations and incorporate these updates +- send the complete set of free blocks "now" (i.e. while the queue is + suspended) to the local allocator. + +Starting xenvmd +--------------- + +`xenvmd` needs to know + +- the device containing the volume group +- the hosts to "connect" to via the shared queues + +The device containing the volume group should be written to a config +file when the SR is plugged. + +`xenvmd` does not remember which hosts it is listening to across crashes, +restarts or master failovers. The `membership_monitor` will keep the +`xenvmd` list in sync with the `PBD.currently_attached` fields. + +Shutting down the local_allocator +--------------------------------- + +The `local_allocator` should be able to crash at any time and recover +afterwards. If the user requests a `PBD.unplug` we can perform a +clean shutdown by: + +- signalling `xenvmd` to suspend the block allocation queue +- arranging for the `local_allocator` to acknowledge the suspension and exit +- when the `xenvmd` sees the acknowlegement, we know that the + `local_allocator` is offline and it doesn't need to poll the queue any more + +Downgrading metadata +-------------------- + +`xenvmd` can be terminated at any time and restarted, since all compound +operations are journalled. + +Downgrade is a special case of shutdown. +To downgrade, we need to stop all hosts allocating and ensure all updates +are flushed to the global LVM metadata. `xenvmd` can shutdown +by: + +- shutting down all `local_allocator`s (see previous section) +- flushing all outstanding block allocations to the LVM redo log +- flushing the LVM redo log to the global LVM metadata + +Queues as rings +--------------- + +We can use a simple ring protocol to represent the queues on the disk. +Each queue will have a single consumer and single producer and reside within +a single logical volume. + +To make diagnostics simpler, we can require the ring to only support `push` +and `pop` of *whole* messages i.e. there can be no partial reads or partial +writes. This means that the `producer` and `consumer` pointers will always +point to valid message boundaries. + +One possible format used by the [prototype](https://github.com/mirage/shared-block-ring/blob/master/lib/ring.ml) is as follows: + +- sector 0: a magic string +- sector 1: producer state +- sector 2: consumer state +- sector 3...: data + +Within the producer state sector we can have: + +- octets 0-7: producer offset: a little-endian 64-bit integer +- octet 8: 1 means "suspend acknowledged"; 0 otherwise + +Within the consumer state sector we can have: + +- octets 0-7: consumer offset: a little-endian 64-bit integer +- octet 8: 1 means "suspend requested"; 0 otherwise + +The consumer and producer pointers point to message boundaries. Each +message is prefixed with a 4 byte length and padded to the next 4-byte +boundary. + +To push a message onto the ring we need to + +- check whether the message is too big to ever fit: this is a permanent + error +- check whether the message is too big to fit given the current free + space: this is a transient error +- write the message into the ring +- advance the producer pointer + +To pop a message from the ring we need to + +- check whether there is unconsumed space: if not this is a transient + error +- read the message from the ring and process it +- advance the consumer pointer + +Journals as queues +------------------ + +When we journal an operation we want to guarantee to execute it never +*or* at-least-once. We can re-use the queue implementation by `push`ing +a description of the work item to the queue and waiting for the +item to be `pop`ped, processed and finally consumed by advancing the +consumer pointer. The journal code needs to check for unconsumed data +during startup, and to process it before continuing. + +Suspending and resuming queues +------------------------------ + +During startup (resync the free blocks) and shutdown (flush the allocations) +we need to suspend and resume queues. The ring protocol can be extended +to allow the *consumer* to suspend the ring by: + +- the consumer asserts the "suspend requested" bit +- the producer `push` function checks the bit and writes "suspend acknowledged" +- the producer also periodically polls the queue state and writes + "suspend acknowledged" (to catch the case where no items are to be pushed) +- after the producer has acknowledged it will guarantee to `push` no more + items +- when the consumer polls the producer's state and spots the "suspend acknowledged", + it concludes that the queue is now suspended. + +The key detail is that the handshake on the ring causes the two sides +to synchronise and both agree that the ring is now suspended/ resumed. + + +Modelling the suspend/resume protocol +------------------------------------- + +To check that the suspend/resume protocol works well enough to be used +to resynchronise the free blocks list on a slave, a simple +[promela model](queue.pml) was created. We model the queue state as +2 boolean flags: + +``` +bool suspend /* suspend requested */ +bool suspend_ack /* suspend acknowledged *./ +``` + +and an abstract representation of the data within the ring: + +``` +/* the queue may have no data (none); a delta or a full sync. + the full sync is performed immediately on resume. */ +mtype = { sync delta none } +mtype inflight_data = none +``` + +There is a "producer" and a "consumer" process which run forever, +exchanging data and suspending and resuming whenever they want. +The special data item `sync` is only sent immediately after a resume +and we check that we never desynchronise with asserts: + +``` + :: (inflight_data != none) -> + /* In steady state we receive deltas */ + assert (suspend_ack == false); + assert (inflight_data == delta); + inflight_data = none +``` +i.e. when we are receiving data normally (outside of the suspend/resume +code) we aren't suspended and we expect deltas, not full syncs. + +The model-checker [spin](http://spinroot.com/spin/whatispin.html) +verifies this property holds. + +Interaction with HA +=================== + +Consider what will happen if a host fails when HA is disabled: + +- if the host is a slave: the VMs running on the host will crash but + no other host is affected. +- if the host is a master: allocation requests from running VMs will + continue provided enough free blocks are cached on the hosts. If a + host eventually runs out of free blocks, then guest I/O will start to + block and VMs may eventually crash. + +Therefore we *recommend* that users enable HA and only disable it +for short periods of time. Note that, unlike other thin-provisioning +implementations, we will allow HA to be disabled. + +Host-local LVs +============== + +When a host calls SMAPI `sr_attach`, it will use `xenvm` to tell `xenvmd` on the +SRmaster to connect to the `local_allocator` on the host. The `xenvmd` +daemon will create the volumes for queues and a volume to represent the +"free blocks" which a host is allowed to allocate. + +Monitoring +========== + +The `xenvmd` process should export RRD datasources over shared +memory named + +- ```sr___free```: the number of free blocks in + the local cache. It's useful to look at this and verify that it doesn't + usually hit zero, since that's when allocations will start to block. + For this reason we should use the `MIN` consolidation function. +- ```sr___requests```: a counter of the number + of satisfied allocation requests. If this number is too high then the quantum + of allocation should be increased. For this reason we should use the + `MAX` consolidation function. +- ```sr___allocations```: a counter of the number of + bytes being allocated. If the allocation rate is too high compared with + the number of free blocks divided by the HA timeout period then the + `SRmaster-allocator` should be reconfigured to supply more blocks with the host. + +Modifications to tapdisk +======================== + +TODO: to be updated by Germano + +```tapdisk``` will be modified to + +- on open: discover the current maximum size of the file/LV (for a file + we assume there is no limit for now) +- read a low-water mark value from a config file ```/etc/tapdisk3.conf``` +- read a very-low-water mark value from a config file ```/etc/tapdisk3.conf``` +- read a Unix domain socket path from a config file ```/etc/tapdisk3.conf``` +- when there is less free space available than the low-water mark: connect + to Unix domain socket and write an "extend" request +- upon receiving the "extend" response, re-read the maximum size of the + file/LV +- when there is less free space available than the very-low-water mark: + start to slow I/O responses and write a single 'error' line to the log. + +The extend request +------------------ + +TODO: to be updated by Germano + +The request has the following format: + +Octet offsets | Name | Description +-----------------|----------|------------ +0,1 | tl | Total length (including this field) of message (in network byte order) +2 | type | The value '0' indicating an extend request +3 | nl | The length of the LV name in octets, including NULL terminator +4,..,4+nl-1 | name | The LV name +4+nl,..,12+nl-1 | vdi_size | The virtual size of the logical VDI (in network byte order) +12+nl,..,20+nl-1 | lv_size | The current size of the LV (in network byte order) +20+nl,..,28+nl-1 | cur_size | The current size of the vhd metadata (in network byte order) + +The extend response +------------------- + +The response is a single byte value "0" which is a signal to re-examime +the LV size. The request will block indefinitely until it succeeds. The +request will block for a long time if + +- the SR has genuinely run out of space. The admin should observe the + existing free space graphs/alerts and perform an SR resize. +- the master has failed and HA is disabled. The admin should re-enable + HA or fix the problem manually. + +The local_allocator +=================== + +There is one `local_allocator` process per plugged PBD. +The process will be +spawned by the SM `sr_attach` call, and shutdown from the `sr_detach` call. + +The `local_allocator` accepts the following configuration (via a config file): + +- `socket`: path to a local Unix domain socket. This is where the `local_allocator` + listens for requests from `tapdisk` +- `allocation_quantum`: number of megabytes to allocate to each tapdisk on request +- `local_journal`: path to a block device or file used for local journalling. This + should be deleted on reboot. +- `free_pool`: name of the LV used to store the host's free blocks +- `devices`: list of local block devices containing the PVs +- `to_LVM`: name of the LV containing the queue of block allocations sent to `xenvmd` +- `from_LVM`: name of the LV containing the queue of messages sent from `xenvmd`. + There are two types of messages: + 1. Free blocks to put into the free pool + 2. Cap requests to remove blocks from the free pool. + +When the `local_allocator` process starts up it will read the host local +journal and + +- re-execute any pending allocation requests from tapdisk +- suspend and resume the `from_LVM` queue to trigger a full retransmit + of free blocks from `xenvmd` + +The procedure for handling an allocation request from tapdisk is: + +1. if there aren't enough free blocks in the free pool, wait polling the + `from_LVM` queue +2. choose a range of blocks to assign to the tapdisk LV from the free LV +3. write the operation (i.e. exactly what we are about to do) to the journal. + This ensures that it will be repeated if the allocator crashes and restarts. + Note that, since the operation may be repeated multiple times, it must be + idempotent. +5. push the block assignment to the `toLVM` queue +6. suspend the device mapper device +7. add/modify the device mapper target +8. resume the device mapper device +9. remove the operation from the local journal (i.e. there's no need to repeat + it now) +10. reply to tapdisk + +Shutting down the local-allocator +--------------------------------- + +The SM `sr_detach` called from `PBD.unplug` will use the `xenvm` CLI to request +that `xenvmd` disconnects from a host. The procedure is: + +1. SM calls `xenvm disconnect host` +2. `xenvm` sends an RPC to `xenvmd` tunnelled through `xapi` +3. `xenvmd` suspends the `to_LVM` queue +4. the `local_allocator` acknowledges the suspend and exits +5. `xenvmd` flushes all updates from the `to_LVM` queue and stops listening + +xenvmd +====== + +`xenvmd` is a daemon running per SRmaster PBD, started in `sr_attach` and +terminated in `sr_detach`. `xenvmd` has a config file containing: + +- `socket`: Unix domain socket where `xenvmd` listens for requests from + `xenvm` tunnelled by `xapi` +- `host_allocation_quantum`: number of megabytes to hand to a host at a time +- `host_low_water_mark`: threshold below which we will hand blocks to a host +- `devices`: local devices containing the PVs + +`xenvmd` continually + +- peeks updates from all the `to_LVM` queues +- calculates how much free space each host still has +- if the size of a host's free pool drops below some threshold: + - choose some free blocks +- if the size of a host's free pool goes above some threshold: + - request a cap of the host's free pool +- writes the change it is going to make to a journal stored in an LV +- pops the updates from the `to_LVM` queues +- pushes the updates to the `from_LVM` queues +- pushes updates to the LVM redo-log +- periodically flush the LVM redo-log to the LVM metadata area + +The membership monitor +====================== + +The role of the membership monitor is to keep the list of `xenvmd` connections +in sync with the `PBD.currently_attached` fields. + +We shall + +- install a ```host-pre-declare-dead``` script to use `xenvm` to send an RPC + to `xenvmd` to forcibly flush (without acknowledgement) the `to_LVM` queue + and destroy the LVs. +- modify XenAPI ```Host.declare_dead``` to call ```host-pre-declare-dead``` before + the VMs are unlocked +- add a ```host-pre-forget``` hook type which will be called just before a Host + is forgotten +- install a ```host-pre-forget``` script to use `xenvm` to call `xenvmd` to + destroy the host's local LVs + +Modifications to LVHD SR +======================== + +- `sr_attach` should: + - if an SRmaster, update the `MGT` major version number to prevent + - Write the xenvmd configuration file (on _all_ hosts, not just SRmaster) + - spawn `local_allocator` +- `sr_detach` should: + - call `xenvm` to request the shutdown of `local_allocator` +- `vdi_deactivate` should: + - call `xenvm` to request the flushing of all the `to_LVM` queues to the + redo log +- `vdi_activate` should: + - if necessary, call `xenvm` to deflate the LV to the minimum size (with some slack) + +Note that it is possible to attach and detach the individual hosts in any order +but when the SRmaster is unplugged then there will be no "refilling" of the host +local free LVs; it will behave as if the master host has failed. + +Modifications to xapi +===================== + +- Xapi needs to learn how to forward xenvm connections to the SR master. +- Xapi needs to start and stop xenvmd at the appropriate times +- We must disable unplugging the PBDs for shared SRs on the pool master + if any other slave has its PBD plugging. This is actually fixing an + issue that exists today - LVHD SRs require the master PBD to be + plugged to do many operations. +- Xapi should provide a mechanism by which the xenvmd process can be killed + once the last PBD for an SR has been unplugged. + +Enabling thin provisioning +========================== + +Thin provisioning will be automatically enabled on upgrade. When the SRmaster +plugs in `PBD` the `MGT` major version number will be bumped to prevent old +hosts from plugging in the SR and getting confused. +When a VDI is activated, it will be deflated to the new low size. + +Disabling thin provisioning +=========================== + +We shall make a tool which will + +- allow someone to downgrade their pool after enabling thin provisioning +- allow developers to test the upgrade logic without fully downgrading their + hosts + +The tool will + +- check if there is enough space to fully inflate all non-snapshot leaves +- unplug all the non-SRmaster `PBD`s +- unplug the SRmaster `PBD`. As a side-effect all pending LVM updates will be + written to the LVM metadata. +- modify the `MGT` volume to have the lower metadata version +- fully inflate all non-snapshot leaves + +Walk-through: upgrade +===================== + +Rolling upgrade should work in the usual way. As soon as the pool master has been +upgraded, hosts will be able to use thin provisioning when new VDIs are attached. +A VM suspend/resume/reboot or migrate will be needed to turn on thin provisioning +for existing running VMs. + +Walk-through: downgrade +======================= + +A pool may be safely downgraded to a previous version without thin provisioning +provided that the downgrade tool is run. If the tool hasn't run then the old +pool will refuse to attach the SR because the metadata has been upgraded. + +Walk-through: after a host failure +================================== + +If HA is enabled: + +- ```xhad``` elects a new master if necessary +- ```Xapi``` on the master will start xenvmd processes for shared thin-lvhd SRs +- the ```xhad``` tells ```Xapi``` which hosts are alive and which have failed. +- ```Xapi``` runs the ```host-pre-declare-dead``` scripts for every failed host +- the ```host-pre-declare-dead``` tells `xenvmd` to flush the `to_LVM` updates +- ```Xapi``` unlocks the VMs and restarts them on new hosts. + +If HA is not enabled: + +- The admin should verify the host is definitely dead +- If the dead host was the master, a new master must be designated. This will + start the xenvmd processes for the shared thin-lvhd SRs. +- the admin must tell ```Xapi``` which hosts have failed with ```xe host-declare-dead``` +- ```Xapi``` runs the ```host-pre-declare-dead``` scripts for every failed host +- the ```host-pre-declare-dead``` tells `xenvmd` to flush the `to_LVM` updates +- ```Xapi``` unlocks the VMs +- the admin may now restart the VMs on new hosts. + +Walk-through: co-operative master transition +============================================ + +The admin calls Pool.designate_new_master. This initiates a two-phase +commit of the new master. As part of this, the slaves will restart, +and on restart each host's xapi will kill any xenvmd that should only +run on the pool master. The new designated master will then restart itself +and start up the xenvmd process on itself. + +Future use of dm-thin? +====================== + +Dm-thin also uses 2 local LVs: one for the "thin pool" and one for the metadata. +After replaying our journal we could potentially delete our host local LVs and +switch over to dm-thin. + +Summary of the impact on the admin +================================== + +- If the VM workload performs a lot of disk allocation, then the admin *should* + enable HA. +- The admin *must* not downgrade the pool without first cleanly detaching the + storage. +- Extra metadata is needed to track thin provisioing, reducing the amount of + space available for user volumes. +- If an SR is completely full then it will not be possible to enable thin + provisioning. +- There will be more fragmentation, but the extent size is large (4MiB) so it + shouldn't be too bad. + +Ring protocols +============== + +Each ring consists of 3 sectors of metadata followed by the data area. The +contents of the first 3 sectors are: + +Sector, Octet offsets | Name | Type | Description +----------------------|-------------|--------|------ +0,0-30 | signature | string | Signature ("mirage shared-block-device 1.0") +1,0-7 | producer | uint64 | Pointer to the end of data written by the producer +1,8 | suspend_ack | uint8 | Suspend acknowledgement byte +2,0-7 | consumer | uint64 | Pointer to the end of data read by the consumer +2,8 | suspend | uint8 | Suspend request byte + + +Note. producer and consumer pointers are stored in little endian +format. + +The pointers are free running byte offsets rounded up to the next +4-byte boundary, and the position of the actual data is found by +finding the remainder when dividing by the size of the data area. The +producer pointer points to the first free byte, and the consumer +pointer points to the byte after the last data consumed. The actual +payload is preceded by a 4-byte length field, stored in little endian +format. When writing a 1 byte payload, the next value of the producer +pointer will therefore be 8 bytes on from the previous - 4 for the +length (which will contain [0x01,0x00,0x00,0x00]), 1 byte for the +payload, and 3 bytes padding. + +A ring is suspended and resumed by the consumer. To suspend, the +consumer first checks that the producer and consumer agree on the +current suspend status. If they do not, the ring cannot be +suspended. The consumer then writes the byte 0x02 into byte 8 of +sector 2. The consumer must then wait for the producer to acknowledge +the suspend, which it will do by writing 0x02 into byte 8 of sector 1. + +The FromLVM ring +---------------- + +Two different types of message can be sent on the FromLVM ring. + +The FreeAllocation message contains the blocks for the free pool. +Example message: + + (FreeAllocation((blocks((pv0(12326 12249))(pv0(11 1))))(generation 2))) + +Pretty-printed: + + (FreeAllocation + ( + (blocks + ( + (pv0(12326 12249)) + (pv0(11 1)) + ) + ) + (generation 2) + ) + ) + +This is a message to add two new sets of extents to the free pool. A +span of length 12249 extents starting at extent 12326, and a span of +length 1 starting from extent 11, both within the physical volume +'pv0'. The generation count of this message is '2'. The semantics of +the generation is that the local allocator must record the generation +of the last message it received since the FromLVM ring was resumed, +and ignore any message with a generated less than or equal to the last +message received. + +The CapRequest message contains a request to cap the free pool at +a maximum size. +Example message: + + (CapRequest((cap 6127)(name host1-freeme))) + +Pretty-printed: + + (CapRequest + ( + (cap 6127) + (name host1-freeme) + ) + ) + +This is a request to cap the free pool at a maximum size of 6127 +extents. The 'name' parameter reflects the name of the LV into which +the extents should be transferred. + +The ToLVM Ring +-------------- + +The ToLVM ring only contains 1 type of message. Example: + + ((volume test5)(segments(((start_extent 1)(extent_count 32)(cls(Linear((name pv0)(start_extent 12328)))))))) + +Pretty-printed: + + ( + (volume test5) + (segments + ( + ( + (start_extent 1) + (extent_count 32) + (cls + (Linear + ( + (name pv0) + (start_extent 12328) + ) + ) + ) + ) + ) + ) + ) + +This message is extending an LV named 'test5' by giving it 32 extents +starting at extent 1, coming from PV 'pv0' starting at extent +12328. The 'cls' field should always be 'Linear' - this is the only +acceptable value. + + +Cap requests +============ + +Xenvmd will try to keep the free pools of the hosts within a range +set as a fraction of free space. There are 3 parameters adjustable +via the config file: + +- low_water_mark_factor +- medium_water_mark_factor +- high_water_mark_factor + +These three are all numbers between 0 and 1. Xenvmd will sum the free +size and the sizes of all hosts' free pools to find the total +effective free size in the VG, `F`. It will then subtract the sizes of +any pending desired space from in-flight create or resize calls `s`. This +will then be divided by the number of hosts connected, `n`, and +multiplied by the three factors above to find the 3 absolute values +for the high, medium and low watermarks. + + {high, medium, low} * (F - s) / n + +When xenvmd notices that a host's free pool size has dropped below +the low watermark, it will be topped up such that the size is equal +to the medium watermark. If xenvmd notices that a host's free pool +size is above the high watermark, it will issue a 'cap request' to +the host's local allocator, which will then respond by allocating +from its free pool into the fake LV, which xenvmd will then delete +as soon as it gets the update. + +Xenvmd keeps track of the last update it has sent to the local +allocator, and will not resend the same request twice, unless it +is restarted. + diff --git a/doc/content/design/thin-lvhd/queue.pml b/doc/content/design/thin-lvhd/queue.pml new file mode 100644 index 00000000000..681f3027f77 --- /dev/null +++ b/doc/content/design/thin-lvhd/queue.pml @@ -0,0 +1,64 @@ +/* queue suspend/resume protocol */ + +/* flags in the shared disk */ +bool suspend /* suspend requested */ +bool suspend_ack /* suspend acknowledged *. + +/* the queue may have no data (none); a delta or a full sync. + the full sync is performed immediately on resume. */ +mtype = { sync delta none } +mtype inflight_data = none + +proctype consumer(){ + + /* get the channel back to a known state by suspending, + resuming and receiving the initial resync */ +resync: + (suspend == suspend_ack) + suspend = true; + (suspend == suspend_ack) +resync2: + /* drop old data */ + inflight_data = none; + suspend = false; + (suspend == suspend_ack) + (inflight_data == sync) + /* receive initial sync */ + inflight_data = none; + do + /* Consumer.pop */ + :: (inflight_data != none) -> + /* In steady state we receive deltas */ + assert (suspend_ack == false); + assert (inflight_data == delta); + inflight_data = none + /* Consumer.suspend */ + :: ((suspend == false)&&(suspend_ack == false)) -> + goto resync + /* Consumer.resume */ + :: ((suspend == true)&&(suspend_ack == true)) -> + goto resync2 + od; +} + +proctype producer(){ + do + /* Producer.state = Running */ + :: ((suspend == false)&&(suspend_ack==true)) -> + suspend_ack = false; + inflight_data = sync + /* Producer.state = Suspended */ + :: ((suspend == true) && (suspend_ack == false)) -> + suspend_ack = true + /* Producer.push */ + :: ((suspend == false) && (suspend_ack == false) && (inflight_data != sync)) -> + inflight_data = delta + od +} + +init { + atomic { + run producer(); + run consumer(); + } +} diff --git a/doc/content/design/thin-lvhd/thin-lvhd.graffle b/doc/content/design/thin-lvhd/thin-lvhd.graffle new file mode 100644 index 0000000000000000000000000000000000000000..b6ddda0dc15c2fb04e03bd45df66587f6db41055 GIT binary patch literal 7315 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 B*H!=k literal 0 HcmV?d00001 diff --git a/doc/content/design/thin-lvhd/xenvmd.graffle b/doc/content/design/thin-lvhd/xenvmd.graffle new file mode 100644 index 0000000000000000000000000000000000000000..42c8cfc9cf26ef6ce611280fc229ce2aae65cd03 GIT binary patch literal 5667 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 string) map status` (read/write); owned by the controller, containing at least the + key `active`, and `key` and `error` when appropriate (see below) +* `(string -> string) map other_config` (read/write) + +New fields in PIF class (automatically linked to the corresponding `tunnel` +fields): + +* `PIF ref set tunnel_access_PIF_of` (read-only) +* `PIF ref set tunnel_transport_PIF_of` (read-only) + +#### Messages + +* `tunnel ref create (PIF ref, network ref)` +* `void destroy (tunnel ref)` + +### Backends + +For clients to determine which network backend is in use (to decide whether +tunnelling functionality is enabled) a key `network_backend` is added to the +`Host.software_version` map on each host. The value of this key can be: + +* `bridge`: the Linux bridging backend is in use; +* `openvswitch`: the [Open vSwitch] backend is in use. + +### Notes + +* The user is responsible for creating tunnel and network objects, associating + VIFs with the right networks, and configuring the physical PIFs, all using + the XenAPI/CLI/XC. + +* The `tunnel.status` field is owned by the controller. It + may be possible to define an RBAC role for the controller, such that only the + controller is able to write to it. + +* The `tunnel.create` message does not take + a tunnel identifier (GRE key). The controller is responsible for assigning + the right keys transparently. When a tunnel has been set up, the controller + will write its key to `tunnel.status:key`, and it will set + `tunnel.status:active` to `"true"` in the same field. + +* In case a tunnel could + not be set up, an error code (to be defined) will be written to + `tunnel.status:error`, and `tunnel.status:active` will be `"false"`. + +Xapi +---- + +### tunnel.create + +* Fails with `OPENVSWITCH_NOT_ACTIVE` if the Open vSwitch networking sub-system + is not active (the host uses linux bridging). +* Fails with `IS_TUNNEL_ACCESS_PIF` if the specified transport PIF is a tunnel access PIF. +* Takes care of creating and connecting the new tunnel and PIF objects. + * Sets a random MAC on the access PIF. + * IP configuration of the tunnel + access PIF is left blank. (The IP configuration on a PIF is normally used for + the interface in dom0. In this case, there is no tunnel interface for dom0 to + use. Such functionality may be added in future.) + * The `tunnel.status:active` + field is initialised to `"false"`, indicating that no actual tunnelling + infrastructure has been set up yet. +* Calls `PIF.plug` on the new tunnel access PIF. + +### tunnel.destroy + +* Calls `PIF.unplug` on the tunnel access PIF. Destroys the `tunnel` and + tunnel access PIF objects. + +### PIF.plug on a tunnel access PIF + +* Fails with `TRANSPORT_PIF_NOT_CONFIGURED` if the underlying transport PIF has + `PIF.ip_configuration_mode = None`, as this interface needs to be configured + for the tunnelling to work. Otherwise, the transport PIF will be plugged. +* Xapi requests `interface-reconfigure` to "bring up" the tunnel access PIF, + which causes it to create a local bridge. +* No link will be made between the + new bridge and the physical interface by `interface-reconfigure`. The + controller is responsible for setting up these links. If the controller is + not available, no links can be created, and the tunnel network degrades to an + internal network (only intra-host connectivity). +* `PIF.currently_attached` is set to `true`. + +### PIF.unplug on a tunnel access PIF + +* Xapi requests `interface-reconfigure` to "bring down" the tunnel PIF, which + causes it to destroy the local bridge. +* `PIF.currently_attached` is set to `false`. + +### PIF.unplug on a tunnel transport PIF + +* Calls `PIF.unplug` on the associated tunnel access PIF(s). + +### PIF.forget on a tunnel access of transport PIF + +* Fails with `PIF_TUNNEL_STILL_EXISTS`. + +### VLAN.create + +* Tunnels can only exist on top of physical/VLAN/Bond PIFs, and not the other + way around. `VLAN.create` fails with `IS_TUNNEL_ACCESS_PIF` if given an + underlying PIF that is a tunnel access PIF. + +### Pool join + +* As for VLANs, when a host joins a pool, it will inherit the tunnels that are + present on the pool master. +* Any tunnels (tunnel and access PIF objects) + configured on the host are removed, which will leave their networks + disconnected (the networks become internal networks). As a joining host is + always a single host, there is no real use for having had tunnels on it, so + this probably will never be an issue. + +The controller +-------------- + +* The controller tracks the `tunnel` class to determine which bridges/networks + require GRE tunnelling. + * On start-up, it calls `tunnel.get_all` to obtain the information about all + tunnels. + * Registers for events on the `tunnel` class to stay up-to-date. +* A tunnel network is organised as a star topology. The controller is free to + decide which host will be the central host ("switching host"). +* If the + current switching host goes down, a new one will be selected, and GRE tunnels + will be reconstructed. +* The controller creates GRE tunnels connecting each + existing Open vSwitch bridge that is associated with the same tunnel network, + after assigning the network a unique GRE key. +* The controller destroys GRE + tunnels if associated Open vSwitch bridges are destroyed. If the destroyed + bridge was on the switching host, and other hosts are still using the same + tunnel network, a new switching host will be selected, and GRE tunnels will + be reconstructed. +* The controller sets `tunnel.status:active` to `"true"` for + all tunnel links that have been set up, and `"false"` if links are broken. +* The controller writes an appropriate error code (to be defined) to + `tunnel.status:error` in case something went wrong. +* When an access PIF is + plugged, and the controller succeeds to set up the tunnelling infrastructure, + it writes the GRE key to `tunnel.status:key` on the associated tunnel object + (at the same time `tunnel.status:active` will be set to `"true"`). +* When the + tunnel infrastructure is not up and running, the controller may remove the + key `tunnel.status:key` (optional; the key should anyway be disregarded if + `tunnel.status:active` is `"false"`). + +CLI +--- + +New `xe` commands (analogous to `xe vlan-`): + +* `tunnel-create` +* `tunnel-destroy` +* `tunnel-list` +* `tunnel-param-get` +* `tunnel-param-list` diff --git a/doc/content/design/vgpu-type-identifiers.md b/doc/content/design/vgpu-type-identifiers.md new file mode 100644 index 00000000000..234a16d4827 --- /dev/null +++ b/doc/content/design/vgpu-type-identifiers.md @@ -0,0 +1,112 @@ +--- +title: VGPU type identifiers +layout: default +design_doc: true +revision: 1 +status: released (7.0) +design_review: 156 +revision_history: +- revision_number: 1 + description: Initial version +--- + +Introduction +------------ + +When xapi starts, it may create a number of VGPU_type objects. These act as +VGPU presets, and exactly which VGPU_type objects are created depends on the +installed hardware and in certain cases the presence of certain files in dom0. + +When deciding which VGPU_type objects need to be created, xapi needs to +determine whether a suitable VGPU_type object already exists, as there should +never be duplicates. At the moment the combination of vendor name and model name +is used as a primary key, but this is not ideal as these values are subject to +change. We therefore need a way of creating a primary key to uniquely identify +VGPU_type objects. + +Identifier +---------- + +We will add a new read-only field to the database: + +- `VGPU_type.identifier (string)` + +This field will contain a string representation of the parameters required to +uniquely identify a VGPU_type. The parameters required can be summed up with the +following OCaml type: + +``` +type nvidia_id = { + pdev_id : int; + psubdev_id : int option; + vdev_id : int; + vsubdev_id : int; +} + +type gvt_g_id = { + pdev_id : int; + low_gm_sz : int64; + high_gm_sz : int64; + fence_sz : int64; + monitor_config_file : string option; +} + +type t = + | Passthrough + | Nvidia of nvidia_id + | GVT_g of gvt_g_id +``` + +When converting this type to a string, the string will always be prefixed with +`0001:` enabling future versioning of the serialisation format. + +For passthrough, the string will simply be: + +`0001:passthrough` + +For NVIDIA, the string will be `nvidia` followed by the four device IDs +serialised as four-digit hex values, separated by commas. If `psubdev_id` is +`None`, the empty string will be used e.g. + +``` +Nvidia { + pdev_id = 0x11bf; + psubdev_id = None; + vdev_id = 0x11b0; + vsubdev_id = 0x109d; +} +``` + +would map to + +`0001:nvidia,11bf,,11b0,109d` + +For GVT-g, the string will be `gvt-g` followed by the physical device ID encoded +as four-digit hex, followed by `low_gm_sz`, `high_gm_sz` and `fence_sz` encoded +as hex, followed by `monitor_config_file` (or the empty string if it is `None`) +e.g. + +``` +GVT_g { + pdev_id = 0x162a; + low_gm_sz = 128L; + high_gm_sz = 384L; + fence_sz = 4L; + monitor_config_file = None; +} +``` + +would map to + +`0001:gvt-g,162a,80,180,4,,` + +Having this string in the database will allow us to do a simple lookup to test +whether a certain VGPU_type already exists. Although it is not currently +required, this string can also be converted back to the type from which it was +generated. + +When deciding whether to create VGPU_type objects, xapi will generate the +identifier string and use it to look for existing VGPU_type objects in the +database. If none are found, xapi will look for existing VGPU_type objects with +the tuple of model name and vendor name. If still none are found, xapi will +create a new VGPU_type object. diff --git a/doc/content/design/virt-hw-platform-vn.md b/doc/content/design/virt-hw-platform-vn.md new file mode 100644 index 00000000000..ec4f21ce4cb --- /dev/null +++ b/doc/content/design/virt-hw-platform-vn.md @@ -0,0 +1,39 @@ +--- +title: Virtual Hardware Platform Version +layout: default +design_doc: true +revision: 1 +status: released (7.0) +--- + +### Background and goal + +Some VMs can only be run on hosts of sufficiently recent versions. + +We want a clean way to ensure that xapi only tries to run a guest VM on a host that supports the "virtual hardware platform" required by the VM. + +### Suggested design + +* In the datamodel, VM has a new integer field "hardware_platform_version" which defaults to zero. +* In the datamodel, Host has a corresponding new integer-list field "virtual_hardware_platform_versions" which defaults to list containing a single zero element (i.e. `[0]` or `[0L]` in OCaml notation). The zero represents the implicit version supported by older hosts that lack the code to handle the Virtual Hardware Platform Version concept. +* When a host boots it populates its own entry from a hardcoded value, currently `[0; 1]` i.e. a list containing the two integer elements `0` and `1`. (Alternatively this could come from a config file.) + * If this new version-handling functionality is introduced in a hotfix, at some point the pool master will have the new functionality while at least one slave does not. An old slave-host that does not yet have software to handle this feature will not set its DB entry, which will therefore remain as `[0]` (maintained in the DB by the master). +* The existing test for whether a VM can run on (or migrate to) a host must include a check that the VM's virtual hardware platform version is in the host's list of supported versions. +* When a VM is made to start using a feature that is available only in a certain virtual hardware platform version, xapi must set the VM's hardware_platform_version to the maximum of that version-number and its current value (i.e. raise if needed). + +For the version we could consider some type other than integer, but a strict ordering is needed. + +### First use-case + +Version 1 denotes support for a certain feature: + +> When a VM starts, if a certain flag is set in VM.platform then XenServer will provide an emulated PCI device which will trigger the guest Windows OS to seek drivers for the device, or updates for those drivers. Thus updated drivers can be obtained through the standard Windows Update mechanism. + +If the PCI device is removed, the guest OS will fail to boot. A VM using this feature must not be migrated to or started on a XenServer that lacks support for the feature. + +Therefore at VM start, we can look at whether this feature is being used; if it is, then if the VM's Virtual Hardware Platform Version is less than 1 we should raise it to 1. + +### Limitation +Consider a VM that requires version 1 or higher. Suppose it is exported, then imported into an old host that does not support this feature. Then the host will not check the versions but will attempt to run the VM, which will then have difficulties. + +The only way to prevent this would be to make a backwards-incompatible change to the VM metadata (e.g. a new item in an enum) so that the old hosts cannot read it, but that seems like a bad idea. diff --git a/doc/content/design/xenopsd_events.md b/doc/content/design/xenopsd_events.md new file mode 100644 index 00000000000..55ee74f7a5b --- /dev/null +++ b/doc/content/design/xenopsd_events.md @@ -0,0 +1,47 @@ +--- +layout: default +title: Process events from xenopsd in a timely manner +design_doc: true +status: proposed +revision: 1 +--- + +# Background + +There is a significant delay between the VM being unpaused and XAPI reporting it +as started during a bootstorm. +It can happen that the VM is able to send UDP packets already, but XAPI still reports it as not started for minutes. + +XAPI currently processes all events from xenopsd in a single thread, the unpause +events get queued up behind a lot of other events generated by the already +running VMs. + +We need to ensure that unpause events from xenopsd get processed in a timely +manner, even if XAPI is busy processing other events. + +# Timely processing of events + +If we process the events in a Round-Robin fashion then `unpause` events are reported in a timely fashion. +We need to ensure that events operating on the same VM are not processed in parallel. + +Xenopsd already has code that does exactly this, the purpose of the [xapi-work-queues refactoring PR](https://github.com/xapi-project/xenopsd/pull/337) is to +reuse this code in XAPI by creating a shared package between xenopsd and xapi: `xapi-work-queues`. + +# xapi-work-queues + +From the documentation of the new [Worker Pool interface](https://edwintorok.github.io/xapi-work-queues/Xapi_work_queues.html): + +A worker pool has a limited number of worker threads. +Each worker pops one tagged item from the queue in a round-robin fashion. +While the item is executed the tag temporarily doesn't participate in round-robin scheduling. +If during execution more items get queued with the same tag they get redirected to a private queue. +Once the item finishes execution the tag will participate in RR scheduling again. + +This ensures that items with the same tag do not get executed in parallel, +and that a tag with a lot of items does not starve the execution of other tags. + +The XAPI side of the changes will [look like this](https://github.com/edwintorok/xen-api/commit/b367bf86d3af4f773db9bf5d1500a4dec0f99bfa?diff=unified#diff-344dc1d17c4663add7fe5500813feef2) + +Known limitations: The active per-VM events should be a small number, this is already ensured in the `push_with_coalesce` / `should_keep` code on the [xenopsd side](https://github.com/xapi-project/xenopsd/blob/master/lib/xenops_server.ml#L441). Events to XAPI from xenopsd should already arrive coalesced. + + diff --git a/doc/content/design/xenprep.md b/doc/content/design/xenprep.md new file mode 100644 index 00000000000..42a393310c6 --- /dev/null +++ b/doc/content/design/xenprep.md @@ -0,0 +1,79 @@ +--- +title: XenPrep +layout: default +design_doc: true +revision: 2 +status: proposed +--- + +### Background +Windows guests should have XenServer-specific drivers installed. As of mid-2015 these have been always been installed and upgraded by an essentially manual process involving an ISO carrying the drivers. We have a plan to enable automation through the standard Windows Update mechanism. This will involve a new additional virtual PCI device being provided to the VM, to trigger Windows Update to fetch drivers for the device. + +There are many existing Windows guests that have drivers installed already. These drivers must be uninstalled before the new drivers are installed (and ideally before the new PCI device is added). To make this easier, we are planning a XenAPI call that will cause the removal of the old drivers and the addition of the new PCI device. + +Since this is only to help with updating old guests, the call may well be removed at some point in the future. + +### Brief high-level design +The XenAPI call will be called `VM.xenprep_start`. It will update the VM record to note that the process has started, and will insert a special ISO into the VM's virtual CD drive. + +That ISO will contain a tool which will be set up to auto-run (if auto-run is enabled in the guest). The tool will: + +1. Lock the CD drive so other Windows programs cannot eject the disc. +2. Uninstall the old drivers. +3. Eject the CD to signal success. +4. Shut down the VM. + +XenServer will interpret the ejection of the CD as a success signal, and when the VM shuts down without the special ISO in the drive, XenServer will: + +1. Update the VM record: + * Remove the mark that shows that the xenprep process is in progress + * Give it the new PCI device: set `VM.auto_update_drivers` to `true`. + * If `VM.virtual_hardware_platform_version` is less than 2, then set it to 2. +2. Start the VM. + +### More details of the xapi-project parts +(The tool that runs in the guest is out of scope for this document.) + +#### Start +The XenAPI call `VM.xenprep_start` will throw a power-state error if the VM is not running. +For RBAC roles, it will be available to "VM Operator" and above. + +It will: + +1. Insert the xenprep ISO into the VM's virtual CD drive. +2. Write `VM.other_config` key `xenprep_progress=ISO_inserted` to record the fact that the xenprep process has been initiated. + +If `xenprep_start` is called on a VM already undergoing xenprep, the call will return successfully but will not do anything. + +If the VM does not have an empty virtual CD drive, the call will fail with a suitable error. + +#### Cancellation +While xenprep is in progress, any request to eject the xenprep ISO (except from inside the guest) will be rejected with a new error "VBD_XENPREP_CD_IN_USE". + +There will be a new XenAPI call `VM.xenprep_abort` which will: + +1. Remove the `xenprep_progress` entry from `VM.other_config`. +2. Make a best-effort attempt to eject the CD. (The guest might prevent ejection.) + +This is not intended for cancellation while the xenprep tool is running, but rather for use before it starts, for example if auto-run is disabled or if the VM has a non-Windows OS. + +#### Completion + +Aim: when the guest shuts down after ejecting the CD, XenServer will start the guest again with the new PCI device. + +Xapi works through the queue of events it receives from xenopsd. It is possible that by the time xapi processes the cd-eject event, the guest might have shut down already. + +When the shutdown (not reboot) event is handled, we shall check whether we need to do anything xenprep-related. If +* The VM `other_config` map has `xenprep_progress` as either of `ISO_inserted` or `shutdown`, and +* The xenprep ISO is no longer in the drive + +then we must (in the specified order) + +1. Update the VM record: + 1. In `VM.other_config` set `xenprep_progress=shutdown` + 2. If `VM.virtual_hardware_platform_version` is less than 2, then set it to 2. + 3. Give it the new PCI device: set `VM.auto_update_drivers` to `true`. +2. Initiate VM start. +3. Remove `xenprep_progress` from `VM.other_config` + +The most relevant code is probably the `update_vm` function in `ocaml/xapi/xapi_xenops.ml` in the `xen-api` repo (or in some function called from there). From 2b43520f8286b68a38c3b3ac715a0cb21dc64d97 Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Fri, 31 May 2024 15:01:45 +0100 Subject: [PATCH 54/99] doc: add info table to design docs Signed-off-by: Rob Hoes --- doc/assets/css/misc.css | 38 ++++++++++++++++++++++ doc/layouts/partials/content-header.html | 40 ++++++++++++++++++++++++ doc/layouts/partials/custom-header.html | 2 ++ 3 files changed, 80 insertions(+) create mode 100644 doc/assets/css/misc.css create mode 100644 doc/layouts/partials/content-header.html create mode 100644 doc/layouts/partials/custom-header.html diff --git a/doc/assets/css/misc.css b/doc/assets/css/misc.css new file mode 100644 index 00000000000..b3142eee351 --- /dev/null +++ b/doc/assets/css/misc.css @@ -0,0 +1,38 @@ +.revision-table { + width: 50%; + margin: 1em auto 1em auto; + font-size: 80%; +} + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label.label-default { + background-color: #777; +} + +.label.label-info { + background-color: #5bc0de; +} + +.label.label-danger { + background-color: #d9534f; +} + +.label.label-warning { + background-color: #f0ad4e; +} + +.label.label-success { + background-color: #5cb85c; +} \ No newline at end of file diff --git a/doc/layouts/partials/content-header.html b/doc/layouts/partials/content-header.html new file mode 100644 index 00000000000..cee4d17c9df --- /dev/null +++ b/doc/layouts/partials/content-header.html @@ -0,0 +1,40 @@ +{{ if eq $.Page.Params.design_doc true }} + + + + + + + + + + + {{ with $.Page.Params.status | lower }} + + {{ end }} + + {{ with $.Page.Params.revision_history }} + + + + {{ range . }} + + + + + {{ end }} + {{ end }} +
Design document
Revisionv{{$.Page.Params.revision}}
Status + {{.}} +
Revision history
v{{.revision_number}}{{.description}}
+{{ end }} diff --git a/doc/layouts/partials/custom-header.html b/doc/layouts/partials/custom-header.html new file mode 100644 index 00000000000..e26070a9468 --- /dev/null +++ b/doc/layouts/partials/custom-header.html @@ -0,0 +1,2 @@ +{{ $style := resources.Get "css/misc.css" }} + From 45b673f80f1ac9202c02c43944701be6b196d303 Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Fri, 31 May 2024 15:53:32 +0100 Subject: [PATCH 55/99] doc: style design doc index Signed-off-by: Rob Hoes --- doc/assets/css/misc.css | 37 +++++++++++++++++++- doc/content/design/_index.md | 2 +- doc/layouts/shortcodes/design_docs_list.html | 34 ++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 doc/layouts/shortcodes/design_docs_list.html diff --git a/doc/assets/css/misc.css b/doc/assets/css/misc.css index b3142eee351..beb5a28e43a 100644 --- a/doc/assets/css/misc.css +++ b/doc/assets/css/misc.css @@ -35,4 +35,39 @@ .label.label-success { background-color: #5cb85c; -} \ No newline at end of file +} + +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; + +} + +.table-striped > tbody > tr:nth-child(odd) { + background-color: #f9f9f9; +} + +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; +} diff --git a/doc/content/design/_index.md b/doc/content/design/_index.md index 59f76755101..e90cfc7b21f 100644 --- a/doc/content/design/_index.md +++ b/doc/content/design/_index.md @@ -3,4 +3,4 @@ title = "Design Documents" menuTitle = "Designs" +++ -{{% children %}} +{{< design_docs_list >}} diff --git a/doc/layouts/shortcodes/design_docs_list.html b/doc/layouts/shortcodes/design_docs_list.html new file mode 100644 index 00000000000..d64334f3ab1 --- /dev/null +++ b/doc/layouts/shortcodes/design_docs_list.html @@ -0,0 +1,34 @@ +

+ + + +{{ range sort $.Page.Pages "Params.status"}} + +{{ end }} + +
+ {{ .Title }} + v{{.Params.revision}} + {{ with .Params.status | lower }} + + {{.}} + + {{ end }} +
\ No newline at end of file From beaaf1c5a10dff2724ba9297187af32d6ad4755b Mon Sep 17 00:00:00 2001 From: Xihuan Yang Date: Tue, 21 May 2024 08:50:09 +0000 Subject: [PATCH 56/99] CP-47928: Add component test for Go SDK Signed-off-by: Xihuan Yang --- ocaml/sdk-gen/component-test/README.md | 10 +- .../jsonrpc-client/go/error_test.go | 187 ++++++ .../jsonrpc-client/go/event_test.go | 91 +++ .../jsonrpc-client/go/main_test.go | 118 +++- .../jsonrpc-client/go/session_test.go | 99 +++ .../jsonrpc-client/go/vm_test.go | 635 ++++++++++++++++++ .../component-test/jsonrpc-server/server.py | 13 +- ocaml/sdk-gen/component-test/run-tests.sh | 2 +- .../sdk-gen/component-test/spec/session.json | 40 -- .../spec/xapi-24/async_vm_import.json | 15 + .../spec/xapi-24/event_from.json | 33 + .../spec/xapi-24/http_error.json | 15 + .../spec/xapi-24/rpc_error.json | 20 + .../spec/xapi-24/session_login.json | 44 ++ .../spec/xapi-24/vm_get_all_records.json | 231 +++++++ .../spec/xapi-24/vm_get_data_sources.json | 26 + .../spec/xapi-24/vm_get_snapshot_time.json | 18 + .../spec/xapi-24/vm_import.json | 15 + .../spec/xapi-24/vm_migrate_send.json | 24 + .../spec/xapi-24/vm_query_data_source.json | 15 + .../vm_retrieve_wlb_recommendations.json | 17 + .../spec/xapi-24/vm_special_value.json | 32 + .../spec/xapi-24/xapi_error.json | 19 + 23 files changed, 1644 insertions(+), 75 deletions(-) create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/error_test.go create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/event_test.go create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/session_test.go create mode 100644 ocaml/sdk-gen/component-test/jsonrpc-client/go/vm_test.go mode change 100644 => 100755 ocaml/sdk-gen/component-test/run-tests.sh delete mode 100644 ocaml/sdk-gen/component-test/spec/session.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/async_vm_import.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/event_from.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/http_error.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/rpc_error.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/session_login.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_all_records.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_data_sources.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_snapshot_time.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_import.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_migrate_send.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_query_data_source.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_retrieve_wlb_recommendations.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/vm_special_value.json create mode 100644 ocaml/sdk-gen/component-test/spec/xapi-24/xapi_error.json diff --git a/ocaml/sdk-gen/component-test/README.md b/ocaml/sdk-gen/component-test/README.md index d60b597c25d..8e68e3e8a6a 100644 --- a/ocaml/sdk-gen/component-test/README.md +++ b/ocaml/sdk-gen/component-test/README.md @@ -41,13 +41,15 @@ spec/ #### jsonrpc-client jsonrpc-client is a client that imports the SDK and runs the functions, following these important details: -1. Add test_id as a customize request header. +1. Single test case in single test spec file + +2. Add test_id as a customize request header. -2. Ensure that the function and params are aligned with the data defined in spec/ directory. +3. Ensure that the function and params are aligned with the data defined in spec/ directory. -3. In order to support test reports, practitioners should use the specific test framework to test SDK, eg: pytest, gotest, junit, xUnit and so on. +4. In order to support test reports, practitioners should use the specific test framework to test SDK, eg: pytest, gotest, junit, xUnit and so on. -4. To support the SDK component test, it recommended to move the SDK generated to a sub directory as a local module for import purposes, eg: +5. To support the SDK component test, it recommended to move the SDK generated to a sub directory as a local module for import purposes, eg: ``` cp -r ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src jsonrpc-client/go/goSDK ``` diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/error_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/error_test.go new file mode 100644 index 00000000000..645c496e5f3 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/error_test.go @@ -0,0 +1,187 @@ +package componenttest + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + "xenapi" +) + +func TestRpcError(t *testing.T) { + //Constuct an rpc client error + data, err := ReadJsonFile("../../spec/xapi-24/rpc_error.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result map[string]interface{} `json:"result,omitempty"` + Error map[string]interface{} `json:"error,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.get_record,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/rpc_error_09"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + testMethod := "VM.get_record" + vmRef := spec.Key.Params[testMethod][1].(string) + _, err = xenapi.VM.GetRecord(session, xenapi.VMRef(vmRef)) + if err == nil { + t.Log(err) + t.Fail() + return + } + + errorString := "call " + testMethod + "() on " + ServerURL + "/jsonrpc status code: 200. Could not decode response body: json: cannot unmarshal string into Go struct field ResponseError.error.code of type int" + var rpcError = errors.New(errorString) + if err.Error() != rpcError.Error() { + t.Log("The expected error is not the same with the returned one!") + t.Fail() + return + } +} + +func TestHttpError(t *testing.T) { + // Construct an error in RPC response that returned for method/parameter check not as expected + data, err := ReadJsonFile("../../spec/xapi-24/http_error.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + //Use a VMRef which is inconsist with in json file to trigger server check error then return HTTP Error + var vmRef xenapi.VMRef = "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13" + var httpError = errors.New("API error: code 500, message Rpc server failed to handle the client request!") + + _, err = xenapi.VM.GetRecord(session, vmRef) + if err == nil { + t.Log("Expected to be error, but no error is detected") + t.Fail() + return + } + + if !strings.Contains(err.Error(), httpError.Error()) { + t.Log("The expected error is not the same with the returned one!") + t.Fail() + return + } +} + +func TestXapiError(t *testing.T) { + // Construct an error that returned from Xapi + data, err := ReadJsonFile("../../spec/xapi-24/xapi_error.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result map[string]interface{} `json:"result,omitempty"` + Error map[string]interface{} `json:"error,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.get_record,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/xapi_error_13"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + inputParams := spec.Key.Params["VM.get_record"] + vmRef := inputParams[1].(string) + + expectedError := spec.Key.ExpectedResult.MethodName.Error + errorCode := expectedError["code"].(float64) + errorMessage := expectedError["message"].(string) + errorData := expectedError["data"].(string) + xapiError := errors.New("API error: code " + fmt.Sprint(errorCode) + ", message " + errorMessage + ", data " + errorData) + + _, err = xenapi.VM.GetRecord(session, xenapi.VMRef(vmRef)) + if err == nil { + t.Log("Expected to be error, but no error is detected") + t.Fail() + return + } + + if !strings.Contains(err.Error(), xapiError.Error()) { + t.Log("The expected error is not the same with the returned one!") + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/event_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/event_test.go new file mode 100644 index 00000000000..56b4379b8cf --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/event_test.go @@ -0,0 +1,91 @@ +package componenttest + +import ( + "encoding/json" + "reflect" + "testing" + "xenapi" +) + +func TestEventFrom(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/event_from.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result xenapi.EventBatch `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"event.from,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/event_from_11"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["event.from"] + const ( + IndexClasses = 1 + IndexToken = 2 + IndexTimeout = 3 + ) + classesInterfaceSlice, ok1 := inputParams[IndexClasses].([]interface{}) + token, ok2 := inputParams[IndexToken].(string) + timeout, ok3 := inputParams[IndexTimeout].(float64) + if !ok1 || !ok2 || !ok3 { + t.Log("Parameter get error from json file") + t.Fail() + return + } + classes, err := ConvertInterfaceSliceToStringSlice(classesInterfaceSlice) + if err != nil { + t.Log(err) + t.Fail() + return + } + + result, err := xenapi.Event.From(session, classes, token, timeout) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go index 466c037deed..eda67d8b592 100644 --- a/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go @@ -1,8 +1,10 @@ -package main +package componenttest import ( + "encoding/json" "flag" "fmt" + "os" "testing" "xenapi" ) @@ -14,39 +16,105 @@ var session *xenapi.Session var USERNAME_FLAG = flag.String("root", "", "the username of the host (e.g. root)") var PASSWORD_FLAG = flag.String("secret", "", "the password of the host") -func TestLoginSuccess(t *testing.T) { - session = xenapi.NewSession(&xenapi.ClientOpts{ +func GetSession(testId string) (*xenapi.Session, error) { + var newSession *xenapi.Session + newSession = xenapi.NewSession(&xenapi.ClientOpts{ URL: ServerURL, Headers: map[string]string{ - "Test-ID": "test_id1", + "Test-ID": testId, }, }) - if session == nil { - fmt.Printf("Failed to get the session") - return - } - _, err := session.LoginWithPassword(*USERNAME_FLAG, *PASSWORD_FLAG, "1.0", "Go sdk component test") + + return newSession, nil +} + +func ReadJsonFile(filePath string) ([]byte, error) { + dataByte, err := os.ReadFile(filePath) if err != nil { - t.Log(err) - t.Fail() - return + fmt.Println(err) + return nil, err } - expectedXapiVersion := "1.20" - getXapiVersion := session.XAPIVersion - if expectedXapiVersion != getXapiVersion { - t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedXapiVersion, getXapiVersion) + return dataByte, nil +} + +func GetTestId(data []byte) (string, error) { + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + return "", err } - var expectedAPIVersion xenapi.APIVersion = xenapi.APIVersion2_21 - getAPIVersion := session.APIVersion - if expectedAPIVersion != getAPIVersion { - t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedAPIVersion, getAPIVersion) + + var firstKey string + for key := range result { + firstKey = key + break } - err = session.Logout() - if err != nil { - t.Log(err) - t.Fail() - return + return firstKey, nil +} + +func ConvertInterfaceSliceToStringSlice(input []interface{}) ([]string, error) { + var output []string + for _, value := range input { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("non-string value found: %v", value) + } + output = append(output, strValue) + } + return output, nil +} + +func ConvertMapToStringMap(input map[string]interface{}) (map[string]string, error) { + output := make(map[string]string) + for key, value := range input { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("non-string value found for key %s: %v", key, value) + } + output[key] = strValue + } + return output, nil +} + +func ConvertMapToVdiMap(input map[string]interface{}) (map[xenapi.VDIRef]xenapi.SRRef, error) { + output := make(map[xenapi.VDIRef]xenapi.SRRef) + for key, value := range input { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("non-string value found for key %s: %v", key, value) + } + output[xenapi.VDIRef(key)] = xenapi.SRRef(strValue) + } + return output, nil +} + +func ConvertMapToVifMap(input map[string]interface{}) (map[xenapi.VIFRef]xenapi.NetworkRef, error) { + output := make(map[xenapi.VIFRef]xenapi.NetworkRef) + for key, value := range input { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("non-string value found for key %s: %v", key, value) + } + output[xenapi.VIFRef(key)] = xenapi.NetworkRef(strValue) + } + return output, nil +} + +func ConvertMapToVgpuMap(input map[string]interface{}) (map[xenapi.VGPURef]xenapi.GPUGroupRef, error) { + output := make(map[xenapi.VGPURef]xenapi.GPUGroupRef) + for key, value := range input { + strValue, ok := value.(string) + if !ok { + return nil, fmt.Errorf("non-string value found for key %s: %v", key, value) + } + output[xenapi.VGPURef(key)] = xenapi.GPUGroupRef(strValue) } + return output, nil +} + +func TestMain(m *testing.M) { + flag.Parse() + exitVal := m.Run() + os.Exit(exitVal) } diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/session_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/session_test.go new file mode 100644 index 00000000000..500d450a5e4 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/session_test.go @@ -0,0 +1,99 @@ +package componenttest + +import ( + "encoding/json" + "testing" + "xenapi" +) + +func TestLoginSuccess(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/session_login.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result xenapi.HostRecord `json:"result,omitempty"` + } + + type MethodResults struct { + MethodResult1 map[string]interface{} `json:"session.login_with_password,omitempty"` + MethodResult2 map[string]interface{} `json:"pool.get_all,omitempty"` + MethodResult3 map[string]interface{} `json:"pool.get_record,omitempty"` + MethodResult4 ResultBody `json:"host.get_record,omitempty"` + MethodResult5 map[string]interface{} `json:"session.logout,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResults `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/session_login_01"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodResult4.Result + + session = xenapi.NewSession(&xenapi.ClientOpts{ + URL: ServerURL, + Headers: map[string]string{ + "Test-ID": testId, + }, + }) + + inputParams := spec.Key.Params["session.login_with_password"] + const ( + IndexVersion = 2 + IndexOriginator = 3 + ) + + version, ok1 := inputParams[IndexVersion].(string) + originator, ok2 := inputParams[IndexOriginator].(string) + if !ok1 || !ok2 { + t.Log("Parameter get error from json file") + t.Fail() + return + } + _, err = session.LoginWithPassword(*USERNAME_FLAG, *PASSWORD_FLAG, version, originator) + if err != nil { + t.Log(err) + t.Fail() + return + } + + expectedXapiVersion := expectedResult.SoftwareVersion["xapi"] + getXapiVersion := session.XAPIVersion + if expectedXapiVersion != getXapiVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedXapiVersion, getXapiVersion) + } + expectedAPIVersion := xenapi.GetAPIVersion(expectedResult.APIVersionMajor, expectedResult.APIVersionMinor) + getAPIVersion := session.APIVersion + if expectedAPIVersion != getAPIVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedAPIVersion, getAPIVersion) + } + + err = session.Logout() + if err != nil { + t.Log(err) + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/vm_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/vm_test.go new file mode 100644 index 00000000000..e4ebd17cd30 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/vm_test.go @@ -0,0 +1,635 @@ +package componenttest + +import ( + "encoding/json" + "math" + "reflect" + "testing" + "time" + "xenapi" +) + +func TestVMGetAllRecords(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_get_all_records.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result map[xenapi.VMRef]xenapi.VMRecord `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.get_all_records,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_get_all_records_02"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + result, err := xenapi.VM.GetAllRecords(session) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMImport(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_import.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result []xenapi.VMRef `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.import,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_import_03"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + inputParams := spec.Key.Params["VM.import"] + const ( + IndexVMImportURL = 1 //string + IndexSrRef = 2 //string + IndexFullRestore = 3 //bool + IndexForce = 4 //bool + ) + VMImportURL, ok1 := inputParams[IndexVMImportURL].(string) + srRef, ok2 := inputParams[IndexSrRef].(string) + fullRestore, ok3 := inputParams[IndexFullRestore].(bool) + force, ok4 := inputParams[IndexForce].(bool) + if !ok1 || !ok2 || !ok3 || !ok4 { + t.Log("Parameter get error from json file") + t.Fail() + return + } + + result, err := xenapi.VM.Import(session, VMImportURL, xenapi.SRRef(srRef), fullRestore, force) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMRetrieveWlbRecommendations(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_retrieve_wlb_recommendations.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result map[xenapi.HostRef][]string `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.retrieve_wlb_recommendations,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_retrieve_wlb_recommendations_04"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["VM.retrieve_wlb_recommendations"] + vmRef, ok := inputParams[1].(string) + if !ok { + t.Log("Parameter get error from json file") + t.Fail() + return + } + + result, err := xenapi.VM.RetrieveWlbRecommendations(session, xenapi.VMRef(vmRef)) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMMigrateSend(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_migrate_send.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result xenapi.VMRef `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.migrate_send,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_migrate_send_05"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["VM.migrate_send"] + const ( + IndexVMRef = 1 + IndexDest = 2 + IndexLive = 3 + IndexVdiMap = 4 + IndexVifMap = 5 + IndexOptions = 6 + IndexVgpuMap = 7 + ) + vmRef, ok1 := inputParams[IndexVMRef].(string) + destOrg, ok2 := inputParams[IndexDest].(map[string]interface{}) + live, ok3 := inputParams[IndexLive].(bool) + vdiMapOrg, ok4 := inputParams[IndexVdiMap].(map[string]interface{}) + vifMapOrg, ok5 := inputParams[IndexVifMap].(map[string]interface{}) + optionsOrg, ok6 := inputParams[IndexOptions].(map[string]interface{}) + vgpuMapOrg, ok7 := inputParams[IndexVgpuMap].(map[string]interface{}) + if !ok1 || !ok2 || !ok3 || !ok4 || !ok5 || !ok6 || !ok7 { + t.Log("Parameter get error from json file") + t.Fail() + return + } + dest, err := ConvertMapToStringMap(destOrg) + if err != nil { + t.Log(err) + t.Fail() + return + } + vdiMap, err := ConvertMapToVdiMap(vdiMapOrg) + if err != nil { + t.Log(err) + t.Fail() + return + } + vifMap, err := ConvertMapToVifMap(vifMapOrg) + if err != nil { + t.Log(err) + t.Fail() + return + } + options, err := ConvertMapToStringMap(optionsOrg) + if err != nil { + t.Log(err) + t.Fail() + return + } + vgpuMap, err := ConvertMapToVgpuMap(vgpuMapOrg) + if err != nil { + t.Log(err) + t.Fail() + return + } + + result, err := xenapi.VM.MigrateSend(session, xenapi.VMRef(vmRef), dest, live, vdiMap, vifMap, options, vgpuMap) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if result != expectedResult { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMGetDataSources(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_get_data_sources.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result []xenapi.DataSourceRecord `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.get_data_sources,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_get_data_sources_06"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["VM.get_data_sources"] + vmRef, ok := inputParams[1].(string) + if !ok { + t.Log("Parameter get error from json file") + t.Fail() + return + } + + result, err := xenapi.VM.GetDataSources(session, xenapi.VMRef(vmRef)) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMGetSnapshotTime(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_get_snapshot_time.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result time.Time `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"VM.get_snapshot_time,omitempty"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/vm_get_snapshot_time_07"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["VM.get_snapshot_time"] + vmRef, ok := inputParams[1].(string) + if !ok { + t.Log("Parameter get error from json file") + t.Fail() + return + } + result, err := xenapi.VM.GetSnapshotTime(session, xenapi.VMRef(vmRef)) + + if err != nil { + t.Log(err) + t.Fail() + return + } + + if result != expectedResult { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMQueryDataSource(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/vm_query_data_source.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + var vmRef xenapi.VMRef = "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13" + + result, err := xenapi.VM.QueryDataSource(session, vmRef, "CPU0 usage") + if err != nil { + t.Log(err) + t.Fail() + return + } + if !math.IsNaN(result) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} + +func TestVMAsyncImport(t *testing.T) { + data, err := ReadJsonFile("../../spec/xapi-24/async_vm_import.json") + if err != nil { + t.Log(err) + t.Fail() + return + } + + testId, err := GetTestId(data) + if err != nil { + t.Log(err) + t.Fail() + return + } + session, err := GetSession(testId) + if err != nil { + t.Log(err) + t.Fail() + return + } + + type ResultBody struct { + Result xenapi.TaskRef `json:"result,omitempty"` + } + + type MethodResult struct { + MethodName ResultBody `json:"Async.VM.import"` + } + + type TestSpecBody struct { + Method []string `json:"method,omitempty"` + Params map[string][]interface{} `json:"params,omitempty"` + ExpectedResult MethodResult `json:"expected_result"` + } + + type TestSpec struct { + Key TestSpecBody `json:"xapi-24/async_vm_import_12"` + } + + var spec TestSpec + if err := json.Unmarshal(data, &spec); err != nil { + t.Log(err) + t.Fail() + return + } + + expectedResult := spec.Key.ExpectedResult.MethodName.Result + + inputParams := spec.Key.Params["Async.VM.import"] + const ( + IndexVMImportURL = 1 //string + IndexSrRef = 2 //string + IndexFullRestore = 3 //bool + IndexForce = 4 //bool + ) + VMImportURL, ok1 := inputParams[IndexVMImportURL].(string) + srRef, ok2 := inputParams[IndexSrRef].(string) + fullRestore, ok3 := inputParams[IndexFullRestore].(bool) + force, ok4 := inputParams[IndexForce].(bool) + if !ok1 || !ok2 || !ok3 || !ok4 { + t.Log("Parameter get error from json file") + t.Fail() + return + } + + result, err := xenapi.VM.AsyncImport(session, VMImportURL, xenapi.SRRef(srRef), fullRestore, force) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if result != expectedResult { + t.Log("The expected error is not the same with the returned one!") + t.Fail() + return + } +} + +func TestVMSpecialValue(t *testing.T) { + session, err := GetSession("xapi-24/vm_special_value_13") + if err != nil { + t.Log(err) + t.Fail() + return + } + + var vmRecord0 = xenapi.VMRecord{ + HVMShadowMultiplier: math.Inf(1), + SnapshotTime: time.Date(2021, time.July, 28, 13, 20, 00, 0, time.UTC), + } + var vmRecord1 = xenapi.VMRecord{ + HVMShadowMultiplier: math.Inf(1), + SnapshotTime: time.Date(2024, time.April, 20, 12, 00, 00, 0, time.UTC), + } + var vmRecord2 = xenapi.VMRecord{ + HVMShadowMultiplier: math.Inf(-1), + SnapshotTime: time.Date(2024, time.April, 20, 12, 00, 00, 0, time.UTC), + } + var vmRecord3 = xenapi.VMRecord{ + HVMShadowMultiplier: math.Inf(-1), + SnapshotTime: time.Date(2024, time.April, 20, 12, 00, 00, 0, time.UTC), + } + var expectedResult = map[xenapi.VMRef]xenapi.VMRecord{ + "OpaqueRef:vmref009-7e0e-411f-1b6c-4308fd33b164": vmRecord0, + "OpaqueRef:vmref010-7e0e-411f-1b6c-4308fd33b164": vmRecord1, + "OpaqueRef:vmref011-7e0e-411f-1b6c-4308fd33b164": vmRecord2, + "OpaqueRef:vmref012-7e0e-411f-1b6c-4308fd33b164": vmRecord3, + } + + result, err := xenapi.VM.GetAllRecords(session) + if err != nil { + t.Log(err) + t.Fail() + return + } + + if !reflect.DeepEqual(result, expectedResult) { + t.Log("The result returned not the same with expected -> The XAPI outcome diverges from the anticipated value.") + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-server/server.py b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py index 8fff806eac1..d7af70d0934 100644 --- a/ocaml/sdk-gen/component-test/jsonrpc-server/server.py +++ b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py @@ -20,10 +20,13 @@ def load_json_files(): dict: A dictionary containing the merged contents of all JSON files. """ data = {} - for filename in os.listdir("spec/"): - if filename.endswith(".json"): - with open(f"spec/{filename}", "r", encoding="utf-8") as f: - data.update(json.load(f)) + for path, _, files in os.walk("spec/"): + for name in files: + filepath = os.path.join(path, name) + if filepath.endswith(".json"): + with open(filepath, "r", encoding="utf-8") as f: + data.update(json.load(f)) + return data @@ -53,7 +56,7 @@ async def handle(request): "error": { "code": 500, "message": "Rpc server failed to handle the client request!", - "data": str(data), + "data": test_id, } } else: diff --git a/ocaml/sdk-gen/component-test/run-tests.sh b/ocaml/sdk-gen/component-test/run-tests.sh old mode 100644 new mode 100755 index 0eed34f0e0c..b2721b1c67c --- a/ocaml/sdk-gen/component-test/run-tests.sh +++ b/ocaml/sdk-gen/component-test/run-tests.sh @@ -17,7 +17,7 @@ start_jsonrpc_go_client() { # ensure that all dependencies are satisfied go mod tidy # build client.go and run it - go test main_test.go -v & + go test -v & JSONRPC_GO_CLIENT_PID=$! } diff --git a/ocaml/sdk-gen/component-test/spec/session.json b/ocaml/sdk-gen/component-test/spec/session.json deleted file mode 100644 index 4916448f3d9..00000000000 --- a/ocaml/sdk-gen/component-test/spec/session.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "test_id1": { - "method": [ - "session.login_with_password", - "pool.get_all", - "pool.get_record", - "host.get_record", - "session.logout" - ], - "params": { - "session.login_with_password": ["", "", "1.0", "Go sdk component test"], - "pool.get_all": ["login successfully"], - "pool.get_record": ["login successfully", "poolref0"], - "host.get_record": ["login successfully", ""], - "session.logout": ["login successfully"] - }, - "expected_result": { - "session.login_with_password": { - "result": "login successfully" - }, - "pool.get_all": { - "result": ["poolref0"] - }, - "pool.get_record": { - "result": {"name_label": "pool0"} - }, - "host.get_record": { - "result": { - "name_label": "host0", - "software_version": {"xapi": "1.20"}, - "API_version_major": "2", - "API_version_minor": "21" - } - }, - "session.logout": { - "result": "logout successfully" - } - } - } -} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/async_vm_import.json b/ocaml/sdk-gen/component-test/spec/xapi-24/async_vm_import.json new file mode 100644 index 00000000000..3d25a4276ce --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/async_vm_import.json @@ -0,0 +1,15 @@ +{ + "xapi-24/async_vm_import_12": { + "method": [ + "Async.VM.import" + ], + "params": { + "Async.VM.import": ["", "http://vm_import_url", "OpaqueRef:5ec2d621-7b62-f571-d38b-754341a973e5", false, false] + }, + "expected_result": { + "Async.VM.import": { + "result": "OpaqueRef:5task621-7b62-f571-d38b-754341a973e5" + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/event_from.json b/ocaml/sdk-gen/component-test/spec/xapi-24/event_from.json new file mode 100644 index 00000000000..b16a42413a9 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/event_from.json @@ -0,0 +1,33 @@ +{ + "xapi-24/event_from_11": { + "method": [ + "event.from" + ], + "params": { + "event.from": ["", ["vm"], "token0", 600.0] + }, + "expected_result": { + "event.from": { + "result": { + "token": "token0", + "validRefCounts": { + "OpaqueRef:5event01-7b62-f571-d38b-754341a973e5": 6 + }, + "events": [ + { + "obj_uuid": "cevnet08-3e97-53bb-4f97-bd039ca8c217", + "ref": "OpaqueRef:5event01-7b62-f571-d38b-754341a973e5", + "class": "vm", + "operation": "add", + "id": 10, + "snapshot": { + "NameLabel": "snapshot0" + }, + "timestamp": "2024-01-01T13:20:11Z" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/http_error.json b/ocaml/sdk-gen/component-test/spec/xapi-24/http_error.json new file mode 100644 index 00000000000..2baa30ca1b1 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/http_error.json @@ -0,0 +1,15 @@ +{ + "xapi-24/http_error_10": { + "method": [ + "VM.get_record" + ], + "params": { + "VM.get_record": ["", ""] + }, + "expected_result": { + "VM.get_record": { + "result": {} + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/rpc_error.json b/ocaml/sdk-gen/component-test/spec/xapi-24/rpc_error.json new file mode 100644 index 00000000000..b7c531b5ed0 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/rpc_error.json @@ -0,0 +1,20 @@ +{ + "xapi-24/rpc_error_09": { + "method": [ + "VM.get_record" + ], + "params": { + "VM.get_record": ["", "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13"] + }, + "expected_result": { + "VM.get_record": { + "result": {}, + "error": { + "code": "abcd", + "message": 123456, + "data": 2.0 + } + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/session_login.json b/ocaml/sdk-gen/component-test/spec/xapi-24/session_login.json new file mode 100644 index 00000000000..d64e68ce768 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/session_login.json @@ -0,0 +1,44 @@ +{ + "xapi-24/session_login_01": { + "method": [ + "session.login_with_password", + "pool.get_all", + "pool.get_record", + "host.get_record", + "session.logout" + ], + "params": { + "session.login_with_password": ["", "", "1.0", "Go SDK component test"], + "pool.get_all": ["OpaqueRef:12345678-4d8a-e4a9-a241-1ea05c1d6707"], + "pool.get_record": ["OpaqueRef:12345678-4d8a-e4a9-a241-1ea05c1d6707", "OpaqueRef:9c81868a-4d8a-e4a9-a241-1ea05c1d6707"], + "host.get_record": ["OpaqueRef:12345678-4d8a-e4a9-a241-1ea05c1d6707", "OpaqueRef:4a0fde9c-5709-4f66-9968-64fb6162bd97"], + "session.logout": ["OpaqueRef:12345678-4d8a-e4a9-a241-1ea05c1d6707"] + }, + "expected_result": { + "session.login_with_password": { + "result": "OpaqueRef:12345678-4d8a-e4a9-a241-1ea05c1d6707" + }, + "pool.get_all": { + "result": ["OpaqueRef:9c81868a-4d8a-e4a9-a241-1ea05c1d6707"] + }, + "pool.get_record": { + "result": { + "master" :"OpaqueRef:4a0fde9c-5709-4f66-9968-64fb6162bd97" + } + }, + "host.get_record": { + "result": { + "name_label": "host0", + "software_version": { + "xapi": "24.11" + }, + "API_version_minor": 21, + "API_version_major": 2 + } + }, + "session.logout": { + "result": "" + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_all_records.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_all_records.json new file mode 100644 index 00000000000..63677ff2b9b --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_all_records.json @@ -0,0 +1,231 @@ +{ + "xapi-24/vm_get_all_records_02": { + "method": [ + "VM.get_all_records" + ], + "params": { + "VM.get_all_records": [""] + }, + "expected_result": { + "VM.get_all_records": { + "result": { + "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13": { + "HVM_boot_params": { + "firmware": "uefi", + "order": "cd" + }, + "HVM_boot_policy": "BIOS order", + "HVM_shadow_multiplier": 1.0, + "NVRAM": { + "EFI-variables": "VkFSUwIAAAAsAAAAA" + }, + "PCI_bus": "", + "PV_args": "", + "PV_bootloader": "", + "PV_bootloader_args": "", + "PV_kernel": "", + "PV_legacy_args": "", + "PV_ramdisk": "", + "VBDs": [ + "OpaqueRef:f7ef1069-2538-089a-2a77-e42e3b9e336c", + "OpaqueRef:c92a38a8-87aa-2d04-622f-84a788d9f9ef", + "OpaqueRef:e3e47cc5-5752-df57-b3a3-89bcc7336397" + ], + "VCPUs_at_startup": 2, + "VCPUs_max": 2, + "VCPUs_params": { + "weight": "256" + }, + "VGPUs": [], + "VIFs": [ + "OpaqueRef:5f1d3810-45d1-0762-81f6-18b198b52e0c" + ], + "VTPMs": [ + "OpaqueRef:60f5a19e-773b-f831-c9c2-35f07b632603" + ], + "VUSBs": [], + "actions_after_crash": "preserve", + "actions_after_reboot": "restart", + "actions_after_shutdown": "destroy", + "actions_after_softreboot": "soft_reboot", + "affinity": "OpaqueRef:NULL", + "allowed_operations": [ + "changing_dynamic_range", + "migrate_send", + "pool_migrate", + "changing_VCPUs_live", + "suspend", + "hard_reboot", + "hard_shutdown", + "clean_reboot", + "clean_shutdown", + "pause", + "checkpoint", + "snapshot" + ], + "appliance": "OpaqueRef:NULL", + "attached_PCIs": [], + "bios_strings": { + "baseboard-asset-tag": "", + "baseboard-location-in-chassis": "", + "baseboard-manufacturer": "", + "baseboard-product-name": "", + "baseboard-serial-number": "", + "baseboard-version": "", + "bios-vendor": "Xen", + "bios-version": "", + "enclosure-asset-tag": "", + "hp-rombios": "", + "oem-1": "Xen", + "oem-2": "MS_VM_CERT/SHA1/bdbeb6e0a816d43fa6d3fe8aaef04c2bad9d3e3d", + "system-manufacturer": "Xen", + "system-product-name": "HVM domU", + "system-serial-number": "", + "system-version": "" + }, + "blobs": {}, + "blocked_operations": {}, + "children": [], + "consoles": [ + "OpaqueRef:87122f55-ce67-7a33-5d93-0d25206e83a0" + ], + "crash_dumps": [], + "current_operations": {}, + "domain_type": "hvm", + "domarch": "", + "domid": 50, + "generation_id": "209202393574174067:5365773919542227445", + "guest_metrics": "OpaqueRef:60d05d5e-4888-ebeb-f070-c57f90410417", + "ha_always_run": false, + "ha_restart_priority": "", + "hardware_platform_version": 2, + "has_vendor_device": false, + "is_a_snapshot": false, + "is_a_template": false, + "is_control_domain": false, + "is_default_template": false, + "is_snapshot_from_vmpp": false, + "is_vmss_snapshot": false, + "last_boot_CPU_flags": { + "features": "1fcbfbff-f7fa3203-2c100800-00000021-00000001-000007ab-00000000-00000000-00101000-bc000400-00000000-00000000-00000000-00000000-00000000-00000000-04000000-00000000-00000000-00000000-00000000-00000000", + "vendor": "GenuineIntel" + }, + "last_booted_record": "", + "memory_dynamic_max": 8589934592, + "memory_dynamic_min": 8589934592, + "memory_overhead": 71303168, + "memory_static_max": 8589934592, + "memory_static_min": 4294967296, + "memory_target": 8589934592, + "metrics": "OpaqueRef:29a46db3-9b13-7df1-788c-638c194d8cf1", + "name_description": "win11-x64", + "name_label": "win11-x64", + "order": 0, + "other_config": { + "base_template_name": "Windows 11", + "import_task": "OpaqueRef:06641b68-0695-5bfa-2fce-ec2c1c914097", + "install-methods": "cdrom", + "mac_seed": "8414a948-97f5-2a8b-2070-c26b95e5d578", + "xenrt-distro": "win11-x64" + }, + "parent": "OpaqueRef:NULL", + "pending_guidances": [], + "pending_guidances_full": [], + "pending_guidances_recommended": [], + "platform": { + "acpi": "1", + "acpi_laptop_slate": "1", + "apic": "true", + "cores-per-socket": "2", + "device-model": "qemu-upstream-uefi", + "device_id": "0002", + "hpet": "true", + "nx": "true", + "pae": "true", + "secureboot": "true", + "timeoffset": "0", + "vga": "std", + "videoram": "8", + "viridian": "true", + "viridian_apic_assist": "true", + "viridian_crash_ctl": "true", + "viridian_reference_tsc": "true", + "viridian_stimer": "true", + "viridian_time_ref_count": "true", + "vtpm": "true" + }, + "power_state": "Running", + "protection_policy": "OpaqueRef:NULL", + "recommendations": "", + "reference_label": "windows-11", + "requires_reboot": false, + "resident_on": "OpaqueRef:4a0fde9c-5709-4f66-9968-64fb6162bd97", + "scheduled_to_be_resident_on": "OpaqueRef:NULL", + "shutdown_delay": 0, + "snapshot_info": {}, + "snapshot_metadata": "", + "snapshot_of": "OpaqueRef:NULL", + "snapshot_schedule": "OpaqueRef:NULL", + "snapshot_time": "2023-07-29T13:20:01Z", + "snapshots": [], + "start_delay": 0, + "suspend_SR": "OpaqueRef:5ec2d621-7b62-f571-d38b-754341a973e5", + "suspend_VDI": "OpaqueRef:NULL", + "tags": [], + "transportable_snapshot_id": "", + "user_version": 1, + "uuid": "c5db1298-3e97-53bb-4f97-bd039ca8c217", + "version": 0, + "xenstore_data": { + "vm-data": "", + "vm-data/mmio-hole-size": "268435456" + } + }, + "OpaqueRef:7cdfad16-7e0e-411f-1b6c-4308fd33b164": { + "HVM_shadow_multiplier": 1.0, + "VBDs": [ + "OpaqueRef:4a54632c-a6b1-5535-4ef4-0d36d85e07df", + "OpaqueRef:56d316f7-e8d0-54f0-2492-a0952c6946c4" + ], + "allowed_operations": [ + "changing_dynamic_range", + "migrate_send", + "pool_migrate", + "changing_VCPUs_live", + "suspend", + "hard_reboot", + "hard_shutdown", + "clean_reboot", + "clean_shutdown", + "pause", + "checkpoint", + "snapshot" + ], + "current_operations": {}, + "domid": 45, + "is_a_template": false, + "memory_static_max": 4294967296, + "name_label": "ubuntu2204", + "platform": { + "acpi": "1", + "apic": "true", + "device-model": "qemu-upstream-compat", + "device_id": "0001", + "hpet": "true", + "nx": "true", + "pae": "true", + "timeoffset": "0", + "vga": "std", + "videoram": "8", + "viridian": "false" + }, + "snapshot_time": "2006-01-02T15:04:05Z", + "suspend_VDI": "OpaqueRef:NULL", + "resident_on": "OpaqueRef:4a0fde9c-5709-4f66-9968-64fb6162bd97", + "uuid": "060aee48-7ded-d2bd-6f0f-f224bd99c960" + } + } + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_data_sources.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_data_sources.json new file mode 100644 index 00000000000..e383743e822 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_data_sources.json @@ -0,0 +1,26 @@ +{ + "xapi-24/vm_get_data_sources_06": { + "method": [ + "VM.get_data_sources" + ], + "params": { + "VM.get_data_sources": ["", "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13"] + }, + "expected_result": { + "VM.get_data_sources": { + "result": [ + { + "enabled": true, + "max": 2.6, + "min": 0.0, + "name_description": "Memory currently allocated to VM", + "name_label": "memory", + "standard": true, + "units": "B", + "value": 0.0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_snapshot_time.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_snapshot_time.json new file mode 100644 index 00000000000..6cfe19be60c --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_get_snapshot_time.json @@ -0,0 +1,18 @@ +{ + "xapi-24/vm_get_snapshot_time_07": { + "method": [ + "VM.get_snapshot_time" + ], + "params": { + "VM.get_snapshot_time": ["", "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13"] + }, + "expected_result": { + "VM.get_snapshot_time": { + "result": "2024-04-20T12:00:00Z" + }, + "session.logout": { + "result": "" + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_import.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_import.json new file mode 100644 index 00000000000..c66d042f640 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_import.json @@ -0,0 +1,15 @@ +{ + "xapi-24/vm_import_03": { + "method": [ + "VM.import" + ], + "params": { + "VM.import": ["", "http://vm_import_url", "OpaqueRef:5ec2d621-7b62-f571-s38r-754341a973e5", false, false] + }, + "expected_result": { + "VM.import": { + "result": ["OpaqueRef:5ec2d621-7b62-f571-v38m-754341a973e5"] + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_migrate_send.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_migrate_send.json new file mode 100644 index 00000000000..ae6167dff85 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_migrate_send.json @@ -0,0 +1,24 @@ +{ + "xapi-24/vm_migrate_send_05": { + "method": [ + "VM.migrate_send" + ], + "params": { + "VM.migrate_send": [ + "", + "OpaqueRef:5vm2d621-7b62-f571-vm00-754341a973e5", + {"name_label": "host1"}, + true, + {"OpaqueRef:5vdid621-7b62-f571-vdi8-754341a973e5": "OpaqueRef:5sr2d621-7b62-f571-s38r-754341a973e5"}, + {"OpaqueRef:9vifb8a9-f6a3-69d2-vifb-7a7b7c81c49e": "OpaqueRef:9network-f6a3-69d2-netw-7a7b7c81c49e"}, + {}, + {"OpaqueRef:9vgpu8a9-f6a3-69d2-vgpu-7a7b7c81c49e": "OpaqueRef:9gpugroup-f6a3-69d2-grou-7a7b7c81c49e"} + ] + }, + "expected_result": { + "VM.migrate_send": { + "result": "OpaqueRef:5vm2d621-7b62-f571-vm01-754341a973e5" + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_query_data_source.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_query_data_source.json new file mode 100644 index 00000000000..b77b42b5c08 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_query_data_source.json @@ -0,0 +1,15 @@ +{ + "xapi-24/vm_query_data_source_08": { + "method": [ + "VM.query_data_source" + ], + "params": { + "VM.query_data_source": ["", "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13", "CPU0 usage"] + }, + "expected_result": { + "VM.query_data_source": { + "result": "NaN" + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_retrieve_wlb_recommendations.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_retrieve_wlb_recommendations.json new file mode 100644 index 00000000000..da1ce4e3136 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_retrieve_wlb_recommendations.json @@ -0,0 +1,17 @@ +{ + "xapi-24/vm_retrieve_wlb_recommendations_04": { + "method": [ + "VM.retrieve_wlb_recommendations" + ], + "params": { + "VM.retrieve_wlb_recommendations": ["", "OpaqueRef:5ec2d621-7b62-f571-v38m-754341a973e5"] + }, + "expected_result": { + "VM.retrieve_wlb_recommendations": { + "result": { + "OpaqueRef:4a0fde9c-5709-4f66-9968-64fb6162bd97": ["host0"] + } + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/vm_special_value.json b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_special_value.json new file mode 100644 index 00000000000..19cc850f876 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/vm_special_value.json @@ -0,0 +1,32 @@ +{ + "xapi-24/vm_special_value_13": { + "method": [ + "VM.get_all_records" + ], + "params": { + "VM.get_all_records": [""] + }, + "expected_result": { + "VM.get_all_records": { + "result": { + "OpaqueRef:vmref009-7e0e-411f-1b6c-4308fd33b164": { + "HVM_shadow_multiplier": Infinity, + "snapshot_time": "1627478400." + }, + "OpaqueRef:vmref010-7e0e-411f-1b6c-4308fd33b164": { + "HVM_shadow_multiplier": "+Infinity", + "snapshot_time": "20240420T12:00:00" + }, + "OpaqueRef:vmref011-7e0e-411f-1b6c-4308fd33b164": { + "HVM_shadow_multiplier": "-Infinity", + "snapshot_time": "20240420T12:00:00Z" + }, + "OpaqueRef:vmref012-7e0e-411f-1b6c-4308fd33b164": { + "HVM_shadow_multiplier":"-Infinity", + "snapshot_time": "2024-04-20T12:00:00Z" + } + } + } + } + } +} \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/spec/xapi-24/xapi_error.json b/ocaml/sdk-gen/component-test/spec/xapi-24/xapi_error.json new file mode 100644 index 00000000000..8c9cdabf6b6 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/xapi-24/xapi_error.json @@ -0,0 +1,19 @@ +{ + "xapi-24/xapi_error_13": { + "method": [ + "VM.get_record" + ], + "params": { + "VM.get_record": ["", "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13"] + }, + "expected_result": { + "VM.get_record": { + "error": { + "code": 1, + "message": "HANDLE_INVALID", + "data": "OpaqueRef:6ef08bce-0bf0-30ff-804f-5f0ee4bbdd13" + } + } + } + } +} \ No newline at end of file From de8f720ffedc4844e45b4b9d4d7b687e71e66fa7 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 15:31:46 +0100 Subject: [PATCH 57/99] CP-49647 use URI for create_misc Signed-off-by: Christian Lindig --- ocaml/xapi/create_misc.ml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ocaml/xapi/create_misc.ml b/ocaml/xapi/create_misc.ml index 546b3cc24d1..a41f8a072e0 100644 --- a/ocaml/xapi/create_misc.ml +++ b/ocaml/xapi/create_misc.ml @@ -302,12 +302,15 @@ and create_domain_zero_console_record_with_protocol ~__context ~domain_zero_ref ~dom0_console_protocol = let console_ref = Ref.make () in let address = - Http.Url.maybe_wrap_IPv6_literal - (Db.Host.get_address ~__context ~self:(Helpers.get_localhost ~__context)) + Db.Host.get_address ~__context ~self:(Helpers.get_localhost ~__context) in let location = - Printf.sprintf "https://%s%s?ref=%s" address Constants.console_uri - (Ref.string_of domain_zero_ref) + Uri.( + make ~scheme:"https" ~host:address ~path:Constants.console_uri + ~query:[("ref", [Ref.string_of domain_zero_ref])] + () + |> to_string + ) in let port = match dom0_console_protocol with From b905868d9bf56db88e522b799c9c16b7c02c1f8d Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 15:36:51 +0100 Subject: [PATCH 58/99] CP-49647 use URI for dbsync_master Signed-off-by: Christian Lindig --- ocaml/xapi/dbsync_master.ml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ocaml/xapi/dbsync_master.ml b/ocaml/xapi/dbsync_master.ml index 5cdc4b9106e..fbe0dc9273a 100644 --- a/ocaml/xapi/dbsync_master.ml +++ b/ocaml/xapi/dbsync_master.ml @@ -95,9 +95,12 @@ let refresh_console_urls ~__context = | "" -> "" | address -> - let address = Http.Url.maybe_wrap_IPv6_literal address in - Printf.sprintf "https://%s%s?ref=%s" address - Constants.console_uri (Ref.string_of console) + Uri.( + make ~scheme:"https" ~host:address ~path:Constants.console_uri + ~query:[("ref", [Ref.string_of console])] + () + |> to_string + ) in Db.Console.set_location ~__context ~self:console ~value:url_should_be ) From 06c10bf4c4011cdaf01ef582747021cdf937c348 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 16:07:04 +0100 Subject: [PATCH 59/99] CP-49647 use URI for export.ml Signed-off-by: Christian Lindig --- ocaml/xapi/export.ml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ocaml/xapi/export.ml b/ocaml/xapi/export.ml index c549fb74295..326efdaf067 100644 --- a/ocaml/xapi/export.ml +++ b/ocaml/xapi/export.ml @@ -883,15 +883,14 @@ let handler (req : Request.t) s _ = (* task when it exits, and we don't want to do that *) try let host = find_host_for_VM ~__context vm_ref in - let address = - Http.Url.maybe_wrap_IPv6_literal - (Db.Host.get_address ~__context ~self:host) - in + let address = Db.Host.get_address ~__context ~self:host in let url = - Printf.sprintf "https://%s%s?%s" address req.Request.uri - (String.concat "&" - (List.map (fun (a, b) -> a ^ "=" ^ b) req.Request.query) - ) + Uri.( + make ~scheme:"https" ~host:address ~path:req.Request.uri + ~query:(List.map (fun (a, b) -> (a, [b])) req.Request.query) + () + |> to_string + ) in info "export VM = %s redirecting to: %s" (Ref.string_of vm_ref) url ; let headers = Http.http_302_redirect url in From 2b8b371783775bc4150aba19faf70749ae4af916 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 16:11:55 +0100 Subject: [PATCH 60/99] CP-49647 use URI for import.ml Signed-off-by: Christian Lindig --- ocaml/xapi/import.ml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ocaml/xapi/import.ml b/ocaml/xapi/import.ml index 222185f13c4..d695b94469a 100644 --- a/ocaml/xapi/import.ml +++ b/ocaml/xapi/import.ml @@ -2472,15 +2472,14 @@ let handler (req : Request.t) s _ = if not (check_sr_availability ~__context sr) then ( debug "sr not available - redirecting" ; let host = find_host_for_sr ~__context sr in - let address = - Http.Url.maybe_wrap_IPv6_literal - (Db.Host.get_address ~__context ~self:host) - in + let address = Db.Host.get_address ~__context ~self:host in let url = - Printf.sprintf "https://%s%s?%s" address req.Request.uri - (String.concat "&" - (List.map (fun (a, b) -> a ^ "=" ^ b) req.Request.query) - ) + Uri.( + make ~scheme:"https" ~host:address ~path:req.Request.uri + ~query:(List.map (fun (a, b) -> (a, [b])) req.Request.query) + () + |> to_string + ) in let headers = Http.http_302_redirect url in debug "new location: %s" url ; From 0f335a68329dc4e91bd743c079362bba84b568f2 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 16:16:10 +0100 Subject: [PATCH 61/99] CP-49647 use URI for importexport.ml Signed-off-by: Christian Lindig --- ocaml/xapi/importexport.ml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ocaml/xapi/importexport.ml b/ocaml/xapi/importexport.ml index f90a8da80ea..12562e53c1b 100644 --- a/ocaml/xapi/importexport.ml +++ b/ocaml/xapi/importexport.ml @@ -501,12 +501,13 @@ module Devicetype = struct end let return_302_redirect (req : Http.Request.t) s address = - let address = Http.Url.maybe_wrap_IPv6_literal address in let url = - Printf.sprintf "https://%s%s?%s" address req.Http.Request.uri - (String.concat "&" - (List.map (fun (a, b) -> a ^ "=" ^ b) req.Http.Request.query) - ) + Uri.( + make ~scheme:"https" ~host:address ~path:req.Http.Request.uri + ~query:(List.map (fun (a, b) -> (a, [b])) req.Http.Request.query) + () + |> to_string + ) in let headers = Http.http_302_redirect url in debug "HTTP 302 redirect to: %s" url ; From 4dbf83211821a3fb3336e73bfc3fb189df304d07 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 10:51:13 +0100 Subject: [PATCH 62/99] CP-49647 use URI for rrd_proxy.ml Signed-off-by: Christian Lindig --- ocaml/xapi/rrdd_proxy.ml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ocaml/xapi/rrdd_proxy.ml b/ocaml/xapi/rrdd_proxy.ml index 21ccabd4b09..68b04862f73 100644 --- a/ocaml/xapi/rrdd_proxy.ml +++ b/ocaml/xapi/rrdd_proxy.ml @@ -25,15 +25,12 @@ module D = Debug.Make (struct let name = "rrdd_proxy" end) open D module Rrdd = Rrd_client.Client -(* Helper methods. Should probably be moved to the Http.Request module. *) -let get_query_string_from_query ~(query : (string * string) list) : string = - String.concat "&" (List.map (fun (k, v) -> k ^ "=" ^ v) query) - let make_url_from_query ~(address : string) ~(uri : string) ~(query : (string * string) list) : string = - let query_string = get_query_string_from_query ~query in - let address = Http.Url.maybe_wrap_IPv6_literal address in - Printf.sprintf "https://%s%s?%s" address uri query_string + Uri.make ~scheme:"https" ~host:address ~path:uri + ~query:(List.map (fun (k, v) -> (k, [v])) query) + () + |> Uri.to_string let make_url ~(address : string) ~(req : Http.Request.t) : string = let open Http.Request in From 8cc0f48873beb74dfa6beb85a708ead3172ef0ec Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 11:23:35 +0100 Subject: [PATCH 63/99] CP-49647 use URI for sm_fs_ops.ml Signed-off-by: Christian Lindig --- ocaml/xapi/sm_fs_ops.ml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ocaml/xapi/sm_fs_ops.ml b/ocaml/xapi/sm_fs_ops.ml index 3a803c5a498..bb67bae01e7 100644 --- a/ocaml/xapi/sm_fs_ops.ml +++ b/ocaml/xapi/sm_fs_ops.ml @@ -43,12 +43,18 @@ let import_vdi_url ~__context ?(prefer_slaves = false) _rpc session_id task_id (* Find a suitable host for the SR containing the VDI *) let sr = Db.VDI.get_SR ~__context ~self:vdi in let host = Importexport.find_host_for_sr ~__context ~prefer_slaves sr in - let address = - Http.Url.maybe_wrap_IPv6_literal (Db.Host.get_address ~__context ~self:host) - in - Printf.sprintf "https://%s%s?vdi=%s&session_id=%s&task_id=%s" address - Constants.import_raw_vdi_uri (Ref.string_of vdi) (Ref.string_of session_id) - (Ref.string_of task_id) + let address = Db.Host.get_address ~__context ~self:host in + Uri.( + make ~scheme:"https" ~host:address ~path:Constants.import_raw_vdi_uri + ~query: + [ + ("vdi", [Ref.string_of vdi]) + ; ("session_id", [Ref.string_of session_id]) + ; ("task_id", [Ref.string_of task_id]) + ] + () + |> to_string + ) (* SCTX-286: thin provisioning is thrown away over VDI.copy, VM.import(VM.export). Return true if the newly created vdi must have zeroes written into it; default to false From c38d52c4334e73490b605c391e4ef164df54c192 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 11:27:55 +0100 Subject: [PATCH 64/99] CP-49647 use URI for xapi_message.ml Signed-off-by: Christian Lindig --- ocaml/xapi/xapi_message.ml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ocaml/xapi/xapi_message.ml b/ocaml/xapi/xapi_message.ml index 95702a49515..50621a9aa9c 100644 --- a/ocaml/xapi/xapi_message.ml +++ b/ocaml/xapi/xapi_message.ml @@ -787,12 +787,14 @@ let handler (req : Http.Request.t) fd _ = (* Redirect if we're not master *) if not (Pool_role.is_master ()) then let url = - Printf.sprintf "https://%s%s?%s" - (Http.Url.maybe_wrap_IPv6_literal - (Pool_role.get_master_address ()) - ) - req.Http.Request.uri - (String.concat "&" (List.map (fun (a, b) -> a ^ "=" ^ b) query)) + Uri.( + make ~scheme:"https" + ~host:(Pool_role.get_master_address ()) + ~path:req.Http.Request.uri + ~query:(List.map (fun (k, v) -> (k, [v])) req.Http.Request.query) + () + |> to_string + ) in Http_svr.headers fd (Http.http_302_redirect url) else (* Get and check query parameters *) From 52f297b230c1aea4dab9185eee0e93f0858d6bfe Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 13:32:36 +0100 Subject: [PATCH 65/99] CP-49647 use URI for xapi_xenops.ml Signed-off-by: Christian Lindig --- ocaml/xapi/xapi_xenops.ml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ocaml/xapi/xapi_xenops.ml b/ocaml/xapi/xapi_xenops.ml index 23801ba7ba5..33bcbb7a958 100644 --- a/ocaml/xapi/xapi_xenops.ml +++ b/ocaml/xapi/xapi_xenops.ml @@ -2118,12 +2118,14 @@ let update_vm ~__context id = (fun (_, state) -> let localhost = Helpers.get_localhost ~__context in let address = - Http.Url.maybe_wrap_IPv6_literal - (Db.Host.get_address ~__context ~self:localhost) + Db.Host.get_address ~__context ~self:localhost in let uri = - Printf.sprintf "https://%s%s" address - Constants.console_uri + Uri.( + make ~scheme:"https" ~host:address + ~path:Constants.console_uri () + |> to_string + ) in let get_uri_from_location loc = try From d03a2cd2e931bc09c50e9278d0bc3d6d00cb6215 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 14:04:58 +0100 Subject: [PATCH 66/99] CP-49647 use URI for xapi_vm_migrate.ml Signed-off-by: Christian Lindig --- ocaml/xapi/xapi_vm_migrate.ml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ocaml/xapi/xapi_vm_migrate.ml b/ocaml/xapi/xapi_vm_migrate.ml index 425de03a5a2..877f7bd1897 100644 --- a/ocaml/xapi/xapi_vm_migrate.ml +++ b/ocaml/xapi/xapi_vm_migrate.ml @@ -399,10 +399,14 @@ let pool_migrate ~__context ~vm ~host ~options = use_compression ~__context options (Helpers.get_localhost ~__context) host in debug "%s using stream compression=%b" __FUNCTION__ compress ; - let ip = Http.Url.maybe_wrap_IPv6_literal address in - let scheme = if !Xapi_globs.migration_https_only then "https" else "http" in + let http = if !Xapi_globs.migration_https_only then "https" else "http" in let xenops_url = - Printf.sprintf "%s://%s/services/xenops?session_id=%s" scheme ip session_id + Uri.( + make ~scheme:http ~host:address ~path:"/services/xenops" + ~query:[("session_id", [session_id])] + () + |> to_string + ) in let vm_uuid = Db.VM.get_uuid ~__context ~self:vm in let xenops_vgpu_map = infer_vgpu_map ~__context vm in From 1c8efd5c76ac39ab1cb210e4e1b12137b383fd4b Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 14:53:12 +0100 Subject: [PATCH 67/99] CP-49647 use URI for xapi_host.ml Signed-off-by: Christian Lindig --- ocaml/xapi/xapi_host.ml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index bb3c718647c..01b76be3d85 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -2574,24 +2574,23 @@ let migrate_receive ~__context ~host ~network ~options:_ = able to do HTTPS migrations yet. *) let scheme = "http" in let sm_url = - Printf.sprintf "%s://%s/services/SM?session_id=%s" scheme - (Http.Url.maybe_wrap_IPv6_literal ip) - new_session_id + Uri.make ~scheme ~host:ip ~path:"services/SM" + ~query:[("session_id", [new_session_id])] + () + |> Uri.to_string in let xenops_url = - Printf.sprintf "%s://%s/services/xenops?session_id=%s" scheme - (Http.Url.maybe_wrap_IPv6_literal ip) - new_session_id + Uri.make ~scheme ~host:ip ~path:"services/xenops" + ~query:[("session_id", [new_session_id])] + () + |> Uri.to_string in let master_address = try Pool_role.get_master_address () with Pool_role.This_host_is_a_master -> Option.get (Helpers.get_management_ip_addr ~__context) in - let master_url = - Printf.sprintf "%s://%s/" scheme - (Http.Url.maybe_wrap_IPv6_literal master_address) - in + let master_url = Uri.make ~scheme ~host:master_address () |> Uri.to_string in [ (Xapi_vm_migrate._sm, sm_url) ; (Xapi_vm_migrate._host, Ref.string_of host) From 561ec186ef40f0e77812ea595d367f900614f011 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Fri, 24 May 2024 09:38:41 +0100 Subject: [PATCH 68/99] CP-49647 use URI for cli_util.ml Signed-off-by: Christian Lindig --- ocaml/xapi-cli-server/cli_util.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocaml/xapi-cli-server/cli_util.ml b/ocaml/xapi-cli-server/cli_util.ml index be9ad66839c..5d7e9ef3e6d 100644 --- a/ocaml/xapi-cli-server/cli_util.ml +++ b/ocaml/xapi-cli-server/cli_util.ml @@ -324,7 +324,7 @@ let rec uri_of_someone rpc session_id = function uri_of_someone rpc session_id Master else let address = Client.Host.get_address ~rpc ~session_id ~self:h in - "https://" ^ address + Uri.(make ~scheme:"https" ~host:address () |> to_string) let error_of_exn e = match e with From 23cab04b7259fdfeccd17db840e54b1fda842240 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Thu, 23 May 2024 15:25:11 +0100 Subject: [PATCH 69/99] CP-49647 use URI for http.ml Signed-off-by: Christian Lindig --- ocaml/libs/http-lib/http.ml | 35 +++++++++++++++-------------------- ocaml/libs/http-lib/http.mli | 3 --- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/ocaml/libs/http-lib/http.ml b/ocaml/libs/http-lib/http.ml index 45df884fb1a..dcd42fa692d 100644 --- a/ocaml/libs/http-lib/http.ml +++ b/ocaml/libs/http-lib/http.ml @@ -1018,32 +1018,27 @@ module Url = struct let params = if params = [] then "" else "?" ^ kvpairs params in uri ^ params - (* Wrap a literal IPv6 address in square brackets; otherwise pass through *) - let maybe_wrap_IPv6_literal addr = - if Unixext.domain_of_addr addr = Some Unix.PF_INET6 then - "[" ^ addr ^ "]" - else - addr - let to_string = function | File {path}, data -> - Printf.sprintf "file:%s%s" path (data_to_string data) (* XXX *) - | Http h, data -> - let userpassat = + (* this should have file:// *) + Printf.sprintf "file:%s%s" path (data_to_string data) + (* XXX *) + | Http h, {uri; query_params= params} -> + let auth = match h.auth with | Some (Basic (username, password)) -> - Printf.sprintf "%s:%s@" username password + Printf.sprintf "%s:%s" username password |> Option.some | _ -> - "" + Option.none in - let colonport = - match h.port with Some x -> Printf.sprintf ":%d" x | _ -> "" - in - Printf.sprintf "http%s://%s%s%s%s" - (if h.ssl then "s" else "") - userpassat - (maybe_wrap_IPv6_literal h.host) - colonport (data_to_string data) + Uri.( + make + ~scheme:(if h.ssl then "https" else "http") + ~host:h.host ?port:h.port ?userinfo:auth ~path:uri + ~query:(List.map (fun (k, v) -> (k, [v])) params) + () + |> to_string + ) let get_uri (_scheme, data) = data.uri diff --git a/ocaml/libs/http-lib/http.mli b/ocaml/libs/http-lib/http.mli index 7483c7ecca0..84326e38012 100644 --- a/ocaml/libs/http-lib/http.mli +++ b/ocaml/libs/http-lib/http.mli @@ -264,9 +264,6 @@ module Url : sig val of_string : string -> t - val maybe_wrap_IPv6_literal : string -> string - (** Wrap a literal IPv6 address in square brackets; otherwise pass through *) - val to_string : t -> string val get_uri : t -> string From 1c4f3a9f461862ad1140df1f2fab92ab6897bb05 Mon Sep 17 00:00:00 2001 From: Christian Lindig Date: Wed, 22 May 2024 15:26:14 +0100 Subject: [PATCH 70/99] CP-49647 use URI for cli_operations Signed-off-by: Christian Lindig --- ocaml/xapi-cli-server/cli_operations.ml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/ocaml/xapi-cli-server/cli_operations.ml b/ocaml/xapi-cli-server/cli_operations.ml index 4f2485e1cb3..54eace11b69 100644 --- a/ocaml/xapi-cli-server/cli_operations.ml +++ b/ocaml/xapi-cli-server/cli_operations.ml @@ -6713,13 +6713,20 @@ let pool_dump_db fd _printer rpc session_id params = let pool = List.hd (Client.Pool.get_all ~rpc ~session_id) in let master = Client.Pool.get_master ~rpc ~session_id ~self:pool in let master_address = - Http.Url.maybe_wrap_IPv6_literal - (Client.Host.get_address ~rpc ~session_id ~self:master) + Client.Host.get_address ~rpc ~session_id ~self:master in let uri = - Printf.sprintf "https://%s%s?session_id=%s&task_id=%s" master_address - Constants.pool_xml_db_sync (Ref.string_of session_id) - (Ref.string_of task_id) + Uri.( + make ~scheme:"https" ~host:master_address + ~path:Constants.pool_xml_db_sync + ~query: + [ + ("session_id", [Ref.string_of session_id]) + ; ("task_id", [Ref.string_of task_id]) + ] + () + |> to_string + ) in debug "%s" uri ; HttpGet (filename, uri) From dab475d26c1187e33a2c212e7f4cf5aaed396a51 Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Mon, 13 May 2024 15:25:16 +0100 Subject: [PATCH 71/99] CP-45235: Support for `xe-cli` to transmit `traceparent` Adds the possibility of `xe` commands to pass a `traceparent`. This can be done through the following ways: - a command arg: `xe --traceparent `; - a environment variable: `TRACEPARENT=`: - specifying `traceparent=` in `.xe`. Priority is given in the same order as above. This should enable creating a single trace from the `xe-cli` caller through `xapi`. Signed-off-by: Gabriel Buica --- ocaml/xapi-cli-server/xapi_cli.ml | 15 ++++++++++++--- ocaml/xe-cli/newcli.ml | 17 ++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ocaml/xapi-cli-server/xapi_cli.ml b/ocaml/xapi-cli-server/xapi_cli.ml index 64161f22442..89a9a0177b4 100644 --- a/ocaml/xapi-cli-server/xapi_cli.ml +++ b/ocaml/xapi-cli-server/xapi_cli.ml @@ -121,7 +121,7 @@ let with_session ~local rpc u p session f = (fun () -> f session) (fun () -> do_logout ()) -let do_rpcs _req s username password minimal cmd session args = +let do_rpcs _req s username password minimal cmd session args tracing = let cmdname = get_cmdname cmd in let cspec = try Hashtbl.find cmdtable cmdname @@ -137,7 +137,8 @@ let do_rpcs _req s username password minimal cmd session args = try let generic_rpc = get_rpc () in (* NB the request we've received is for the /cli. We need an XMLRPC request for the API *) - Tracing.with_tracing ~name:("xe " ^ cmdname) @@ fun tracing -> + Tracing.with_tracing ~parent:tracing ~name:("xe " ^ cmdname) + @@ fun tracing -> let req = Xmlrpc_client.xmlrpc ~version:"1.1" ~tracing "/" in let rpc = generic_rpc req s in if do_forward then @@ -188,6 +189,14 @@ let uninteresting_cmd_postfixes = ["help"; "-get"; "-list"] let exec_command req cmd s session args = let params = get_params cmd in + let tracing = + Option.bind + Http.Request.(req.traceparent) + Tracing.SpanContext.of_traceparent + |> Option.map (fun span_context -> + Tracing.Tracer.span_of_span_context span_context (get_cmdname cmd) + ) + in let minimal = if List.mem_assoc "minimal" params then bool_of_string (List.assoc "minimal" params) @@ -248,7 +257,7 @@ let exec_command req cmd s session args = params ) ) ; - do_rpcs req s u p minimal cmd session args + do_rpcs req s u p minimal cmd session args tracing let get_line str i = try diff --git a/ocaml/xe-cli/newcli.ml b/ocaml/xe-cli/newcli.ml index fcb5c4272f8..6d8a55590d4 100644 --- a/ocaml/xe-cli/newcli.ml +++ b/ocaml/xe-cli/newcli.ml @@ -31,6 +31,8 @@ let xapipasswordfile = ref "" let xapiport = ref None +let traceparent = ref None + let get_xapiport ssl = match !xapiport with None -> if ssl then 443 else 80 | Some p -> p @@ -66,7 +68,7 @@ exception Usage let usage () = error "Usage: %s [-s server] [-p port] ([-u username] [-pw password] or \ - [-pwf ]) \n" + [-pwf ]) [--traceparent traceparent] \n" Sys.argv.(0) ; error "\n\ @@ -208,6 +210,8 @@ let parse_args = | "debugonfail" -> ( xedebugonfail := try bool_of_string v with _ -> false ) + | "traceparent" -> + traceparent := Some v | _ -> raise Not_found ) ; @@ -234,6 +238,8 @@ let parse_args = Some ("debugonfail", "true", xs) | "-h" :: h :: xs -> Some ("server", h, xs) + | "--traceparent" :: h :: xs -> + Some ("traceparent", h, xs) | _ -> None in @@ -286,6 +292,10 @@ let parse_args = List.rev !l in let extras_rest = process_args extras in + (*if traceparent is set as env var update it after we process the extras.*) + Option.iter + (fun tp -> traceparent := Some tp) + (Sys.getenv_opt Tracing.EnvHelpers.traceparent_key) ; let help = ref false in let args' = List.filter (fun s -> s <> "-help" && s <> "--help") args in if List.length args' < List.length args then help := true ; @@ -300,7 +310,7 @@ let parse_args = debug_channel := Some tmpch ) in - args_rest @ extras_rest @ rcs_rest @ !reserve_args + (args_rest @ extras_rest @ rcs_rest @ !reserve_args, !traceparent) let exit_status = ref 1 @@ -790,7 +800,7 @@ let main () = Printf.printf "ThinCLI protocol: %d.%d\n" major minor ; exit 0 ) ; - let args = parse_args args in + let args, traceparent = parse_args args in (* All the named args are taken as permitted filename to be uploaded *) let permitted_filenames = get_permit_filenames args in if List.length args < 1 then @@ -803,6 +813,7 @@ let main () = in let args = String.concat "\n" args in Printf.fprintf oc "User-agent: xe-cli/Unix/%d.%d\r\n" major minor ; + Option.iter (Printf.fprintf oc "traceparent: %s\r\n") traceparent ; Printf.fprintf oc "content-length: %d\r\n\r\n" (String.length args) ; Printf.fprintf oc "%s" args ; flush_all () ; From f2a78b56e74d7d5188048fdf592026453b32f913 Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Tue, 4 Jun 2024 14:07:34 +0100 Subject: [PATCH 72/99] doc: add design review links (historical) Signed-off-by: Rob Hoes --- doc/layouts/partials/content-header.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/layouts/partials/content-header.html b/doc/layouts/partials/content-header.html index cee4d17c9df..9043445a3a4 100644 --- a/doc/layouts/partials/content-header.html +++ b/doc/layouts/partials/content-header.html @@ -25,6 +25,14 @@ {{ end }} + {{ with $.Page.Params.design_review }} + + Review + +
#{{.}} + + + {{ end }} {{ with $.Page.Params.revision_history }} Revision history From 52ffb8b8248728fbecdde19c29420b160c36c58f Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Tue, 4 Jun 2024 14:14:10 +0100 Subject: [PATCH 73/99] doc: RDP design: fix list nesting Signed-off-by: Rob Hoes --- doc/content/design/RDP.md | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/content/design/RDP.md b/doc/content/design/RDP.md index 401e6642b38..8793a3066d6 100644 --- a/doc/content/design/RDP.md +++ b/doc/content/design/RDP.md @@ -62,20 +62,20 @@ The function strings are named with "request" (rather than, say, "enable_rdp" or Note that the current behaviour depends on some global options: "Enable Remote Desktop console scanning" and "Automatically switch to the Remote Desktop console when it becomes available". -1. When tools are not installed: - * As of XenCenter 6.5, the RDP button is absent. -2. When tools are installed but RDP is not switched on in the guest: - 1. If "Enable Remote Desktop console scanning" is on: - * The RDP button is present but greyed out. (It seems to sometimes read "Switch to Remote Desktop" and sometimes read "Looking for guest console...": I haven't yet worked out the difference). - * We scan the RDP port to detect when RDP is turned on - 2. If "Enable Remote Desktop console scanning" is off: - * The RDP button is enabled and reads "Switch to Remote Desktop" -3. When tools are installed and RDP is switched on in the guest: - 1. If "Enable Remote Desktop console scanning" is on: - * The RDP button is enabled and reads "Switch to Remote Desktop" - * If "Automatically switch" is on, we switch to RDP immediately we detect it - 2. If "Enable Remote Desktop console scanning" is off: - * As above, the RDP button is enabled and reads "Switch to Remote Desktop" +1. When tools are not installed: + * As of XenCenter 6.5, the RDP button is absent. +2. When tools are installed but RDP is not switched on in the guest: + 1. If "Enable Remote Desktop console scanning" is on: + * The RDP button is present but greyed out. (It seems to sometimes read "Switch to Remote Desktop" and sometimes read "Looking for guest console...": I haven't yet worked out the difference). + * We scan the RDP port to detect when RDP is turned on + 2. If "Enable Remote Desktop console scanning" is off: + * The RDP button is enabled and reads "Switch to Remote Desktop" +3. When tools are installed and RDP is switched on in the guest: + 1. If "Enable Remote Desktop console scanning" is on: + * The RDP button is enabled and reads "Switch to Remote Desktop" + * If "Automatically switch" is on, we switch to RDP immediately we detect it + 2. If "Enable Remote Desktop console scanning" is off: + * As above, the RDP button is enabled and reads "Switch to Remote Desktop" #### New behaviour on XenServer versions that support RDP control @@ -85,14 +85,14 @@ Note that the current behaviour depends on some global options: "Enable Remote D 4. The XenCenter option "Enable Remote Desktop console scanning" should change to read "Enable Remote Desktop console scanning (XenServer 6.5 and earlier)" 5. The XenCenter option "Automatically switch to the Remote Desktop console when it becomes available" should be enabled even when "Enable Remote Desktop console scanning" is off. 6. When tools are not installed: - * As above, the RDP button should be absent. + * As above, the RDP button should be absent. 7. When tools are installed but RDP is not switched on in the guest: - * The RDP button should be enabled and read "Turn on Remote Desktop" - * If pressed, it should launch a dialog with the following wording: "Would you like to turn on Remote Desktop in this VM, and then connect to it over Remote Desktop? [Yes] [No]" - * That button should turn on RDP, wait for RDP to become enabled, and switch to an RDP connection. It should do this even if "Automatically switch" is off. + * The RDP button should be enabled and read "Turn on Remote Desktop" + * If pressed, it should launch a dialog with the following wording: "Would you like to turn on Remote Desktop in this VM, and then connect to it over Remote Desktop? [Yes] [No]" + * That button should turn on RDP, wait for RDP to become enabled, and switch to an RDP connection. It should do this even if "Automatically switch" is off. 8. When tools are installed and RDP is switched on in the guest: - * The RDP button should be enabled and read "Switch to Remote Desktop" - * If "Automatically switch" is on, we should switch to RDP immediately - * There is no need for us to provide UI to switch RDP off again + * The RDP button should be enabled and read "Switch to Remote Desktop" + * If "Automatically switch" is on, we should switch to RDP immediately + * There is no need for us to provide UI to switch RDP off again 9. We should also test the case where RDP has been switched on in the guest before the tools are installed. From e5bb639bcb4eff7d4498f4d156d5a58488a5f04b Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Thu, 16 May 2024 13:58:56 +0100 Subject: [PATCH 74/99] CP-48995: Instrument `XenAPI.py` to submit traceparent Instrument `XenAPI.py` to submit the current traceparent back into xapi if it can import `opentelemetry`. Currently, we don't see the traces of `sm` calling back to `xapi` using `XenAPI.py`. This will instrument `XenAPI.py` to pass a traceparent into `xapi` when opentelemetry is available. Signed-off-by: Gabriel Buica --- python3/packages/observer.py | 5 +++++ scripts/examples/python/XenAPI/XenAPI.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/python3/packages/observer.py b/python3/packages/observer.py index 076fa986a9a..ad41b89848f 100644 --- a/python3/packages/observer.py +++ b/python3/packages/observer.py @@ -371,6 +371,11 @@ def _patch_module(module_name): # If there are no configs, or an exception is raised, span and patch_module # are not overridden and will be the defined no-op functions. span, patch_module = _init_tracing(observer_configs, observer_config_dir) + + # If tracing is now operational, explicity set "OTEL_SDK_DISABLED" to "false". + # In our case, different from the standard, we want the tracing disabled by + # default, so if the env variable is not set the noop implementation is used. + os.environ["OTEL_SDK_DISABLED"] = "false" except Exception as exc: syslog.error("Exception while setting up tracing, running script untraced: %s", exc) span, patch_module = _span_noop, _patch_module_noop diff --git a/scripts/examples/python/XenAPI/XenAPI.py b/scripts/examples/python/XenAPI/XenAPI.py index ab868e92188..0211fe5e9c8 100644 --- a/scripts/examples/python/XenAPI/XenAPI.py +++ b/scripts/examples/python/XenAPI/XenAPI.py @@ -55,6 +55,7 @@ # -------------------------------------------------------------------- import gettext +import os import socket import sys @@ -65,6 +66,15 @@ import http.client as httplib import xmlrpc.client as xmlrpclib +otel = False +try: + if os.environ["OTEL_SDK_DISABLED"] == "false": + from opentelemetry import propagate + from opentelemetry.trace.propagation import set_span_in_context, get_current_span + otel = True + +except Exception: + pass translation = gettext.translation('xen-xm', fallback = True) @@ -101,7 +111,19 @@ def connect(self): class UDSTransport(xmlrpclib.Transport): def add_extra_header(self, key, value): self._extra_headers += [ (key,value) ] + def with_tracecontext(self): + if otel: + headers = {} + # pylint: disable=possibly-used-before-assignment + ctx = set_span_in_context(get_current_span()) + # pylint: disable=possibly-used-before-assignment + propagators = propagate.get_global_textmap() + propagators.inject(headers, ctx) + self._extra_headers = [] + for k, v in headers.items(): + self.add_extra_header(k, v) def make_connection(self, host): + self.with_tracecontext() return UDSHTTPConnection(host) def notimplemented(name, *args, **kwargs): From adba6ee387b86c711faf6ede69df31084efc3237 Mon Sep 17 00:00:00 2001 From: Colin James Date: Wed, 5 Jun 2024 10:36:35 +0100 Subject: [PATCH 75/99] Update datamodel_lifecycle.ml Signed-off-by: Colin James --- ocaml/idl/datamodel_lifecycle.ml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ocaml/idl/datamodel_lifecycle.ml b/ocaml/idl/datamodel_lifecycle.ml index 5044adec27b..14365c257f8 100644 --- a/ocaml/idl/datamodel_lifecycle.ml +++ b/ocaml/idl/datamodel_lifecycle.ml @@ -133,8 +133,14 @@ let prototyped_of_message = function Some "22.27.0" | "host", "set_numa_affinity_policy" -> Some "24.0.0" + | "VM", "get_secureboot_readiness" -> + Some "24.15.0-next" + | "VM", "set_uefi_mode" -> + Some "24.15.0-next" | "VM", "restart_device_models" -> Some "23.30.0" + | "pool", "get_guest_secureboot_readiness" -> + Some "24.15.0-next" | "pool", "set_ext_auth_max_threads" -> Some "23.27.0" | "pool", "set_local_auth_max_threads" -> From 18d17c483cbfb28ac64ea9c977a64310bef861f6 Mon Sep 17 00:00:00 2001 From: xueqingz <52194917+xueqingz@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:44:10 +0800 Subject: [PATCH 76/99] CP-49768: Update GO SDK README file (#5671) Signed-off-by: xueqingz --- ocaml/sdk-gen/README.md | 4 ++-- ocaml/sdk-gen/go/README.md | 39 ++++++++++++++++---------------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/ocaml/sdk-gen/README.md b/ocaml/sdk-gen/README.md index fb4d71650bf..fa45a1c3803 100644 --- a/ocaml/sdk-gen/README.md +++ b/ocaml/sdk-gen/README.md @@ -1,10 +1,10 @@ # XenAPI Software Development Kit (SDK) -The SDK consists of five components, one for each of C, C#, Java, PowerShell, +The SDK consists of six components, one for each of C, C#, Go, Java, PowerShell, and Python, exposing the individual XenAPI calls as first-class functions in the target language. -The source code for the C, C#, Java, and PowerShell SDK is autogenerated from the +The source code for the C, C#, Go, Java, and PowerShell SDK is autogenerated from the XenAPI's datamodel. The generation code is written in OCaml and is contained in this directory. diff --git a/ocaml/sdk-gen/go/README.md b/ocaml/sdk-gen/go/README.md index c8ba2a4d62b..aa173a6b5d1 100644 --- a/ocaml/sdk-gen/go/README.md +++ b/ocaml/sdk-gen/go/README.md @@ -11,7 +11,7 @@ equally well to Go. In particular, the SDK Guide and the Management API Guide are ideal for developers wishing to use XenServer SDK for Go. XenServer SDK for Go is free software. You can redistribute and modify it under the -terms of the BSD 2-Clause license. See LICENSE.txt for details. +terms of the BSD 2-Clause license. See LICENSE for details. ## Reference @@ -40,35 +40,28 @@ This library requires Go 1.22 or greater. This archive contains the following folders that are relevant to Go developers: -- `XenServerGo\src`: contains the Go source files can be used as the local module in a Go project. +- `XenServerGo\src`: contains all Go source files which can be used as a local module for other Go projects. Every API object is associated with one Go file. ## Getting Started Extract the contents of this archive. -A. To set up the local go module: +A. Navigate to the extracted `XenServer-SDK\XenServerGo\src` directory and copy the whole folder `src` into your Go project directory. - 1. Create a new folder in your Go project, eg. `XenServerGo` - 2. Copy all files in `XenServerGo\src` to the new folder +B. To use XenServer Go SDK as a local Go module, update one line into the `go.mod` file under your Go project: -B. To use the XenServer module for Go in your Go project: +``` +replace xenapi => ./src +``` +You can then import this XenServer SDK Go module with the following command: - 1. Add the following lines to your go.mod file: +``` +import "xenapi" +``` - ``` - replace /XenServerGo => ./XenServerGo - ``` +C. Before building your project, run the following Go commands. - 2. Run the command: - - ``` - go mod tidy - ``` - - 3. Use the XenServer module for Go in file as follows: - - ``` - import ( - xenapi "/XenServerGo" - ) - ``` +``` +go get -u all +go mod tidy + ``` From c39726e894ce4524287ba6684a4d506a154a8a2c Mon Sep 17 00:00:00 2001 From: Colin James Date: Wed, 5 Jun 2024 10:37:51 +0100 Subject: [PATCH 77/99] CP-49249: Implement SMAPIv3 CBT Forwarding Implements the changes required to forward CBT-related functionality to the SMAPIv3 storage plugins, which now support CBT for XFS and GFS2 storage repositories. The length of the changed blocks requested is hardcoded as a negative number, which is interpreted by the related plugins as requesting the changes for the entire length of the VDIs being compared. SMAPIv1 does not support this functionality but we retain it here with this behaviour in case we want to make the functionality explicit in future. Signed-off-by: Colin James --- ocaml/xapi-storage-script/main.ml | 47 ++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/ocaml/xapi-storage-script/main.ml b/ocaml/xapi-storage-script/main.ml index f708bb30dfc..0ad8b900e3b 100644 --- a/ocaml/xapi-storage-script/main.ml +++ b/ocaml/xapi-storage-script/main.ml @@ -1507,6 +1507,49 @@ let bind ~volume_script_dir = S.SR.list sr_list ; (* SR.reset is a no op in SMAPIv3 *) S.SR.reset (fun _ _ -> Deferred.Result.return () |> wrap) ; + let ( let* ) = ( >>>= ) in + let vdi_enable_cbt_impl dbg sr vdi = + wrap + @@ + let* sr = Attached_SRs.find sr in + let vdi = Storage_interface.Vdi.string_of vdi in + return_volume_rpc (fun () -> Volume_client.enable_cbt volume_rpc dbg sr vdi) + in + S.VDI.enable_cbt vdi_enable_cbt_impl ; + let vdi_disable_cbt_impl dbg sr vdi = + wrap + @@ + let* sr = Attached_SRs.find sr in + let vdi = Storage_interface.Vdi.string_of vdi in + return_volume_rpc (fun () -> Volume_client.disable_cbt volume_rpc dbg sr vdi) + in + S.VDI.disable_cbt vdi_disable_cbt_impl ; + let vdi_list_changed_blocks_impl dbg sr vdi vdi' = + wrap + @@ + let* sr = Attached_SRs.find sr in + let vdi, vdi' = Storage_interface.Vdi.(string_of vdi, string_of vdi') in + let ( let* ) = ( >>= ) in + let* result = + return_volume_rpc (fun () -> + (* Negative lengths indicate that we want the full length. *) + Volume_client.list_changed_blocks volume_rpc dbg sr vdi vdi' 0L (-1) + ) + in + let proj_bitmap r = r.Xapi_storage.Control.bitmap in + return (Result.map ~f:proj_bitmap result) + in + S.VDI.list_changed_blocks vdi_list_changed_blocks_impl ; + let vdi_data_destroy_impl dbg sr vdi = + wrap + @@ + let* sr = Attached_SRs.find sr in + let vdi = Storage_interface.Vdi.string_of vdi in + return_volume_rpc (fun () -> + Volume_client.data_destroy volume_rpc dbg sr vdi + ) + in + S.VDI.data_destroy vdi_data_destroy_impl ; let u name _ = failwith ("Unimplemented: " ^ name) in S.get_by_name (u "get_by_name") ; S.VDI.compose (u "VDI.compose") ; @@ -1514,13 +1557,11 @@ let bind ~volume_script_dir = S.DATA.MIRROR.receive_start (u "DATA.MIRROR.receive_start") ; S.UPDATES.get (u "UPDATES.get") ; S.SR.update_snapshot_info_dest (u "SR.update_snapshot_info_dest") ; - S.VDI.data_destroy (u "VDI.data_destroy") ; S.DATA.MIRROR.list (u "DATA.MIRROR.list") ; S.TASK.stat (u "TASK.stat") ; S.VDI.remove_from_sm_config (u "VDI.remove_from_sm_config") ; S.DP.diagnostics (u "DP.diagnostics") ; S.TASK.destroy (u "TASK.destroy") ; - S.VDI.list_changed_blocks (u "VDI.list_changed_blocks") ; S.DP.destroy (u "DP.destroy") ; S.VDI.add_to_sm_config (u "VDI.add_to_sm_config") ; S.VDI.similar_content (u "VDI.similar_content") ; @@ -1529,7 +1570,6 @@ let bind ~volume_script_dir = S.DATA.MIRROR.receive_finalize (u "DATA.MIRROR.receive_finalize") ; S.DP.create (u "DP.create") ; S.VDI.set_content_id (u "VDI.set_content_id") ; - S.VDI.disable_cbt (u "VDI.disable_cbt") ; S.DP.attach_info (u "DP.attach_info") ; S.TASK.cancel (u "TASK.cancel") ; S.VDI.attach (u "VDI.attach") ; @@ -1538,7 +1578,6 @@ let bind ~volume_script_dir = S.DATA.MIRROR.stat (u "DATA.MIRROR.stat") ; S.TASK.list (u "TASK.list") ; S.VDI.get_url (u "VDI.get_url") ; - S.VDI.enable_cbt (u "VDI.enable_cbt") ; S.DATA.MIRROR.start (u "DATA.MIRROR.start") ; S.Policy.get_backend_vm (u "Policy.get_backend_vm") ; S.DATA.copy_into (u "DATA.copy_into") ; From f970909e8f0e08006e631756562fdb868148cfbd Mon Sep 17 00:00:00 2001 From: Danilo Del Busso Date: Wed, 5 Jun 2024 11:46:21 +0100 Subject: [PATCH 78/99] CA-393866: Add support for Infinity in Java SDK parser Signed-off-by: Danilo Del Busso --- .../src/main/java/com/xensource/xenapi/JsonRpcClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/JsonRpcClient.java b/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/JsonRpcClient.java index 38ba22db148..b77cd815fb5 100644 --- a/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/JsonRpcClient.java +++ b/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/JsonRpcClient.java @@ -30,6 +30,7 @@ package com.xensource.xenapi; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -212,6 +213,7 @@ protected JsonRpcResponse sendRequest(String methodCall, Object[] methodP private void initializeObjectMapperConfiguration() { var dateHandlerModule = new SimpleModule("DateHandler"); dateHandlerModule.addDeserializer(Date.class, new CustomDateDeserializer()); + this.objectMapper.enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS.mappedFeature()); this.objectMapper.registerModule(dateHandlerModule); } From f12b9b2d98aa40831e8bee33189a1fc924598c1c Mon Sep 17 00:00:00 2001 From: Vincent Liu Date: Tue, 4 Jun 2024 13:27:09 +0100 Subject: [PATCH 79/99] CA-393507: Default cluster_stack value Add a `@default` attribute to the `cluster_stack` field in the cluster config. This field was introduced in #c1bd0e31a but causes RPU to fail since it is not optional, and while xapi-clusterd was unmarshalling an old db (which contains cluster config), it cannot find this field, and throws an exception. Adding `@default` can make the rpc library to fill this field in when it is missing, solving the above problem. Signed-off-by: Vincent Liu --- ocaml/xapi-idl/cluster/cluster_interface.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocaml/xapi-idl/cluster/cluster_interface.ml b/ocaml/xapi-idl/cluster/cluster_interface.ml index 1d28bef0586..e83e69363f2 100644 --- a/ocaml/xapi-idl/cluster/cluster_interface.ml +++ b/ocaml/xapi-idl/cluster/cluster_interface.ml @@ -132,7 +132,7 @@ type cluster_config = { ; config_version: int64 ; cluster_token_timeout_ms: int64 ; cluster_token_coefficient_ms: int64 - ; cluster_stack: Cluster_stack.t + ; cluster_stack: Cluster_stack.t [@default Corosync2] } [@@deriving rpcty] From cb0e5506ccc58ad5d4e1ee608825a095743e54a8 Mon Sep 17 00:00:00 2001 From: Ross Lagerwall Date: Fri, 7 Jun 2024 16:35:11 +0100 Subject: [PATCH 80/99] Remove fix_firewall.sh The last reference to fix_firewall.sh was removed in e696eb8306a3 back in 2011 and the script doesn't appear to be used anywhere else so remove it. Signed-off-by: Ross Lagerwall --- scripts/Makefile | 1 - scripts/fix_firewall.sh | 24 ------------------------ 2 files changed, 25 deletions(-) delete mode 100644 scripts/fix_firewall.sh diff --git a/scripts/Makefile b/scripts/Makefile index d2ab8f5d381..a6afb71ab2b 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -88,7 +88,6 @@ install: mkdir -p $(DESTDIR)/etc/sysconfig $(IPROG) sysconfig-xapi $(DESTDIR)/etc/sysconfig/xapi $(IPROG) nbd-firewall-config.sh $(DESTDIR)$(LIBEXECDIR) - $(IPROG) fix_firewall.sh $(DESTDIR)$(OPTDIR)/bin $(IPROG) update-ca-bundle.sh $(DESTDIR)$(OPTDIR)/bin mkdir -p $(DESTDIR)$(OPTDIR)/debug $(IPROG) debug_ha_query_liveset $(DESTDIR)$(OPTDIR)/debug diff --git a/scripts/fix_firewall.sh b/scripts/fix_firewall.sh deleted file mode 100644 index 7a486257883..00000000000 --- a/scripts/fix_firewall.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Citrix Systems 2008. All rights reserved. -# - -set -e - -# Insert a firewall rule to allow traffic to pass through the guest-installer network - -CHAIN=xapi-INPUT -IFACE=$1 # bridge name of guest installer network -OP=$2 # if == start, then start up the firewall, else stop it. - -# Flush any rules that are already there: -iptables -F $CHAIN &> /dev/null || true -iptables -D INPUT -j $CHAIN &> /dev/null || true -iptables -X $CHAIN &> /dev/null || true - -# Insert the new rule - anything coming from the -if [[ "${OP}" == "start" ]]; then - iptables -N $CHAIN - iptables -I INPUT 1 -j $CHAIN - iptables -A $CHAIN -i $IFACE -j ACCEPT -fi From 31564ab433682ae3a51e86d68d3109e4f75e5ece Mon Sep 17 00:00:00 2001 From: Rob Hoes Date: Tue, 11 Jun 2024 12:40:26 +0000 Subject: [PATCH 81/99] CA-393119: Don't use HTTPS for localhost migrations This is unnecessary overhead for traffic that does not actually hit the network. Signed-off-by: Rob Hoes --- ocaml/xapi/xapi_vm_migrate.ml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ocaml/xapi/xapi_vm_migrate.ml b/ocaml/xapi/xapi_vm_migrate.ml index 877f7bd1897..8fb445aace1 100644 --- a/ocaml/xapi/xapi_vm_migrate.ml +++ b/ocaml/xapi/xapi_vm_migrate.ml @@ -380,6 +380,9 @@ let infer_vgpu_map ~__context ?remote vm = let pool_migrate ~__context ~vm ~host ~options = Pool_features.assert_enabled ~__context ~f:Features.Xen_motion ; let dbg = Context.string_of_task __context in + let localhost = Helpers.get_localhost ~__context in + if host = localhost then + info "This is a localhost migration" ; let open Xapi_xenops_queue in let queue_name = queue_of_vm ~__context ~self:vm in let module XenopsAPI = (val make_client queue_name : XENOPS) in @@ -395,11 +398,14 @@ let pool_migrate ~__context ~vm ~host ~options = .assert_valid_ip_configuration_on_network_for_host ~__context ~self:network ~host in - let compress = - use_compression ~__context options (Helpers.get_localhost ~__context) host - in + let compress = use_compression ~__context options localhost host in debug "%s using stream compression=%b" __FUNCTION__ compress ; - let http = if !Xapi_globs.migration_https_only then "https" else "http" in + let http = + if !Xapi_globs.migration_https_only && host <> localhost then + "https" + else + "http" + in let xenops_url = Uri.( make ~scheme:http ~host:address ~path:"/services/xenops" From e3055ca700fbfcc20b3fd3900917ad971a4bb8ba Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Tue, 11 Jun 2024 15:15:26 +0100 Subject: [PATCH 82/99] CP-49828: Remove iovirt script Not used anymore. Signed-off-by: Frediano Ziglio --- scripts/Makefile | 1 - scripts/plugins/iovirt | 848 ----------------------------------------- 2 files changed, 849 deletions(-) delete mode 100755 scripts/plugins/iovirt diff --git a/scripts/Makefile b/scripts/Makefile index a6afb71ab2b..020cbbeff49 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -135,7 +135,6 @@ install: $(IPROG) plugins/perfmon $(DESTDIR)$(PLUGINDIR) $(IPROG) plugins/extauth-hook $(DESTDIR)$(PLUGINDIR) $(IPROG) plugins/extauth-hook-AD.py $(DESTDIR)$(PLUGINDIR) - $(IPROG) plugins/iovirt $(DESTDIR)$(PLUGINDIR) $(IPROG) plugins/install-supp-pack $(DESTDIR)$(PLUGINDIR) $(IPROG) plugins/disk-space $(DESTDIR)$(PLUGINDIR) $(IPROG) plugins/firewall-port $(DESTDIR)$(PLUGINDIR) diff --git a/scripts/plugins/iovirt b/scripts/plugins/iovirt deleted file mode 100755 index c70c9a007fa..00000000000 --- a/scripts/plugins/iovirt +++ /dev/null @@ -1,848 +0,0 @@ -#!/usr/bin/env python -# -# THIS IS AN EXPERIMENTAL, UNSUPPORTED PLUGIN FOR DEMONSTRATION AND TEST ONLY. -# PLEASE DO NOT USE IT FOR PRODUCTION ENVIRONMENTS. -# THIS PLUGIN CAN BE REMOVED OR MODIFIED AT ANY TIME WITHOUT PREVIOUS WARNING. -# -# A plugin for managing IO virtualization including SR-IOV -# -# Each VM has pass-through VFs configured using the "pci" and "sriovmacs" -# other-config keys. Each of these is a comma separated list of entries -# of the form "/" for pci and "/" for sriovmacs - - -import XenAPI, inventory -import XenAPIPlugin -import os -import os.path -import stat -import glob -import re -import random -import subprocess -import xml.dom.minidom -import syslog - -ipcmd = "/sbin/ip" -hookscripts = ["/etc/xapi.d/vm-pre-start/vm-pre-start-iovirt", - "/etc/xapi.d/vm-pre-reboot/vm-pre-reboot-iovirt" -] - -re_virtfn = re.compile("virtfn(\d+)$") -re_netdev = re.compile("(eth\d+)$") -re_hex16 = re.compile("^[0-9a-f]{4}$") -re_hex16x = re.compile("^0x([0-9a-f]{4})$") - -def _install_hook_script(): - # Ensure that the hook script(s) used to configure SR-IOV VF MAC and - # VLANs is present. If the script isn't present create it. This - # function will be called whenever a VF is assigned. The intention - # is that the hook is not used for users not using this plugin because - # the additional API calls will add a small overhead to the VM.start - # time which may hurt some use cases. - for hookscript in hookscripts: - if os.path.exists(hookscript): - continue - syslog.syslog("Creating iovirt hook script at %s" % (hookscript)) - hookdir = os.path.dirname(hookscript) - if not os.path.exists(hookdir): - os.makedirs(hookdir) - f = file(hookscript, "w") - f.write("""#!/bin/bash -# -# Call the iovirt plugin to set up SR-IOV VF MAC and VLAN config -# if required for this VF. - -PLUGIN=iovirt -FN=prep_for_vm - -for i -do - case "$i" in - -vmuuid) shift; VMUUID=$1; shift;; - esac -done - -if [ -z "$VMUUID" ]; then - logger -t $(basename $0) "VM UUID not found in args" -fi - -. @INVENTORY@ - -if [ -z "$INSTALLATION_UUID" ]; then - logger -t $(basename $0) "Could not determine host UUID" -fi - -xe host-call-plugin plugin=$PLUGIN fn=$FN host-uuid=$INSTALLATION_UUID args:uuid=$VMUUID -""" - ) - f.close() - os.chmod(hookscript, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - -def _get_vfs(): - # Find PCI devices with virtfns, find all virtfns associated with - # network devices. Return dictionary of pciid => (vf#, ethdev) - vfnodes = glob.glob("/sys/bus/pci/devices/*/virtfn*") - vflist = {} - for vfnode in vfnodes: - vfpciid = os.path.basename(os.readlink(vfnode)) - r = re_virtfn.search(vfnode) - if not r: - raise "Failed to parse VF number for %s" % (vfnode) - vfnum = r.group(1) - netdevs = glob.glob("%s/physfn/net/eth*" % (vfnode)) - if len(netdevs) != 1: - raise "Unexpected length of netdev list for %s: %s" % (vfnode, str(netdevs)) - r = re_netdev.search(netdevs[0]) - if not r: - raise "Error parsing %s for net device" % (netdevs[0]) - netdev = r.group(1) - vflist[vfpciid] = (vfnum, netdev) - return vflist - -def _get_devices(vendorid, deviceid): - # Return device instances with the specified vendorid:deviceid - # Returns a dictionary of pciid => (vendorid, deviceid) - # Used for managing the assignment of non-SRIOV pools of devices. - if not re_hex16.match(vendorid): - raise "PCI vendor ID '%s' not in expected format" % (vendorid) - if not re_hex16.match(deviceid): - raise "PCI device ID '%s' not in expected format" % (deviceid) - devices = {} - devnodes = glob.glob("/sys/bus/pci/devices/*") - for devnode in devnodes: - pciid = os.path.basename(devnode) - vendor, device = _get_vendor_and_device_ids(pciid) - if vendor == vendorid and device == deviceid: - devices[pciid] = (vendor, device) - return devices - -def _get_vendor_and_device_ids(pciid): - devnode = ("/sys/bus/pci/devices/%s" % (pciid)) - vendor = None - device = None - if os.path.exists("%s/vendor" % (devnode)): - f = file("%s/vendor" % (devnode)) - d = f.read() - f.close() - vendorid = re_hex16x.search(d) - if vendorid: - vendor = vendorid.group(1) - if os.path.exists("%s/device" % (devnode)): - f = file("%s/device" % (devnode)) - d = f.read() - f.close() - deviceid = re_hex16x.search(d) - if deviceid: - device = deviceid.group(1) - return vendor, device - -def _get_assignments(session): - # Return a dict of PCI devices assigned to VMs. This currently assumes - # a pool of one host. In a pool this will be overly restrictive (i.e. - # a VM started on another host will be reported as having one of our - # host's devices) but it'll do for now. - # Returns a dictionary PCIID => (VMUUID, index, vendorid, deviceid, mac, vlan) - # where mac and vlan are only (optionally) available for SR-IOV VFs. - expr = 'field "is_a_template" = "false" and field "is_a_snapshot" = "false"' - vms = session.xenapi.VM.get_all_records_where(expr) - devices = {} - for vm in vms.values(): - # Ignore VMs with no PCI passthrough config or an empty config - if 'pci' not in vm['other_config']: - continue - if not vm['other_config']['pci']: - continue - vmuuid = vm['uuid'] - assignments = _get_vm_assignments(session, vmuuid) - for index in assignments.keys(): - devices[assignments[index][0]] = (vmuuid, - index, - assignments[index][1], - assignments[index][2], - assignments[index][3], - assignments[index][4]) - return devices - -def enable_iommu(session, args): - rc = os.system("@OPTDIR@/libexec/xen-cmdline --set-xen iommu=1") - return str(rc == 0) - -#def list_assigned_vfs(session, args): -# """List VFs on this host that are assigned to VMs that can run here.""" -# x = _get_assignments(session) -# return `x` - -def _get_vm_assignments(session, vmuuid): - # Return a dictionary of index => (PCIID, vendorid, deviceid, MAC, VLAN) - # for assignments for the specified VM. - # This includes SR-IOV NIC VFs and other PCI pass through - # devices. (MAC and VLAN only includes for the former) - re_pci = re.compile("^(\d+)/([0-9a-f:\.]+)$") - re_vlan = re.compile("^(\d+)/([0-9]+)$") - re_mac = re.compile("^(\d+)/([0-9a-fA-F:]+)$") - - vmref = session.xenapi.VM.get_by_uuid(vmuuid) - vm = session.xenapi.VM.get_record(vmref) - assignments = {} - - # Parse the SR-IOV MAC config string - macs = {} - if 'sriovmacs' in vm['other_config'] and vm['other_config']['sriovmacs']: - for x in vm['other_config']['sriovmacs'].split(","): - r = re_mac.search(x) - if not r: - raise Exception("Failed to parse MAC config '%s' for VM %s" % (x, vmuuid)) - macs[int(r.group(1))] = r.group(2) - - # Parse the SR-IOV VLAN config string - vlans = {} - if 'sriovvlans' in vm['other_config'] and vm['other_config']['sriovvlans']: - for x in vm['other_config']['sriovvlans'].split(","): - r = re_vlan.search(x) - if not r: - raise Exception("Failed to parse VLAN config '%s' for VM %s" % (x, vmuuid)) - vlans[int(r.group(1))] = r.group(2) - - # Parse the PCI config string - if 'pci' in vm['other_config'] and vm['other_config']['pci']: - for x in vm['other_config']['pci'].split(","): - r = re_pci.search(x) - if not r: - raise Exception("Failed to parse PCI config '%s' for VM %s" % (x, vmuuid)) - vendorid, deviceid = _get_vendor_and_device_ids(r.group(2)) - if int(r.group(1)) in macs: - mac = macs[int(r.group(1))] - else: - mac = None - if int(r.group(1)) in vlans: - vlan = vlans[int(r.group(1))] - else: - vlan = None - assignments[int(r.group(1))] = (r.group(2), vendorid, deviceid, mac, vlan) - - return assignments - -def _randomMAC(): - """Return a random MAC in the locally administered range""" - o1 = (random.randint(0, 63) << 2) | 2 - o2 = random.randint(0, 255) - o3 = random.randint(0, 255) - o4 = random.randint(0, 255) - o5 = random.randint(0, 255) - o6 = random.randint(0, 255) - return "%02x:%02x:%02x:%02x:%02x:%02x" % (o1, o2, o3, o4, o5, o6) - -def _set_vm_assignments(session, vmuuid, assignments): - # Set the PCI (and MAC and VLAN for SR-IOV) assignments for the - # specified VM removing any existing config first. assignments is a - # dictionary index => (pciid, vendorid, deviceid, mac, vlan) where - # vendorid and deviceid need not be set. - indexlist = assignments.keys() - indexlist.sort() - pcilist = [] - maclist = [] - vlanlist = [] - for i in indexlist: - pcilist.append("%u/%s" % (i, assignments[i][0])) - if assignments[i][3]: - maclist.append("%u/%s" % (i, assignments[i][3])) - if assignments[i][4]: - vlanlist.append("%u/%s" % (i, assignments[i][4])) - pci = ",".join(pcilist) - mac = ",".join(maclist) - vlan = ",".join(vlanlist) - vmref = session.xenapi.VM.get_by_uuid(vmuuid) - try: - session.xenapi.VM.remove_from_other_config(vmref, "pci") - except: - pass - try: - session.xenapi.VM.remove_from_other_config(vmref, "sriovmacs") - except: - pass - try: - session.xenapi.VM.remove_from_other_config(vmref, "sriovvlans") - except: - pass - if len(pci) > 0: - session.xenapi.VM.add_to_other_config(vmref, "pci", pci) - if len(mac) > 0: - session.xenapi.VM.add_to_other_config(vmref, "sriovmacs", mac) - if len(vlan) > 0: - session.xenapi.VM.add_to_other_config(vmref, "sriovvlans", vlan) - -def assign_free_vf(session, args): - """Assign a free VF on this host to the specified VM.""" - - # Ensure the hook script exists to configure VFs before VM.start - _install_hook_script() - - assigned = _get_assignments(session) - vfs = _get_vfs() - for vfid in assigned.keys(): - if vfid in vfs: - del vfs[vfid] - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - ethdev = None - vlan = None - pifref = None - - # Allow the caller to specify a the eth device from which the VF should - # be allocated - if "ethdev" in args: - ethdev = args["ethdev"] - - # Specify by network UUID. If this is a VLAN network then this forces the - # specification of a VLAN tag for the VF. - if "nwuuid" in args: - nwuuid = args["nwuuid"] - nwref = session.xenapi.network.get_by_uuid(nwuuid) - pifrefs = session.xenapi.network.get_PIFs(nwref) - # Find PIF for this host - hostuuid = inventory.get_localhost_uuid() - hostref = session.xenapi.host.get_by_uuid(hostuuid) - for pref in pifrefs: - if session.xenapi.PIF.get_host(pref) == hostref: - pifref = pref - break - if not pifref: - raise "Could not find PIF record for network %s on this host" % (nwuuid) - - # Specify by PIF UUID. If this is a VLAN PIF then this forces the - # specification of a VLAN tag for the VF. - if "pifuuid" in args: - pifuuid = args["pifuuid"] - pifref = session.xenapi.PIF.get_by_uuid(pifuuid) - - if pifref: - v = str(session.xenapi.PIF.get_VLAN(pifref)) - if v and v != "-1": - vlan = v - ethdev = session.xenapi.PIF.get_device(pifref) - - # If no ethdev, PIF or network is specified then reject - if not ethdev: - raise "Must specify eth device by device, PIF or network." - - # If the caller specified a pass through index use that otherwise - # find the first one not currently configured. - ourassignments = _get_vm_assignments(session, vmuuid) - if "index" in args: - index = int(args["index"]) - else: - i = 0 - while True: - if not i in ourassignments.keys(): - index = i - break - i = i + 1 - - # Use the user specified MAC or create a local adminstered one - if "mac" in args: - mac = args["mac"] - else: - mac = _randomMAC() - - if "vlan" in args: - if vlan and args["vlan"] != vlan: - raise "Cannot override PIF VLAN %s" % (vlan) - vlan = args["vlan"] - - # Choose a suitable VF. Preference is for lower numbered VFs - revdict = {} - for vfid in vfs.keys(): - rvf, rdev = vfs[vfid] - rkey = "%08u%s" % (int(rvf), rdev) - revdict[rkey] = vfid - revdictkeys = revdict.keys() - revdictkeys.sort() - vfidlist = [revdict[x] for x in revdictkeys] - myid = None - for vfid in vfidlist: - if not ethdev: - myid = vfid - break - if ethdev == vfs[vfid][1]: - myid = vfid - break - if not myid: - if ethdev: - raise "No spare VF on %s" % (ethdev) - raise "No spare VF" - - # Set up the config for the VM. No need to fill out the vendor and - # device id fields for this. - ourassignments[index] = (myid, None, None, mac, vlan) - _set_vm_assignments(session, vmuuid, ourassignments) - - vfnum, vfeth = vfs[myid] - - dom = xml.dom.minidom.Document() - element = dom.createElement("iovirt") - dom.appendChild(element) - - entry = dom.createElement("vf") - element.appendChild(entry) - - subentry = dom.createElement("pciid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(myid)) - - subentry = dom.createElement("device") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfeth)) - - subentry = dom.createElement("vfnum") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfnum)) - - if mac: - subentry = dom.createElement("mac") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(mac)) - if vlan: - subentry = dom.createElement("vlan") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vlan)) - - aentry = dom.createElement("assigned") - entry.appendChild(aentry) - - subentry = dom.createElement("vm") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vmuuid)) - - subentry = dom.createElement("index") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(str(index))) - - return dom.toprettyxml() - -def unassign_vf(session, args, genericpci=False): - """Unassign a VF/device from a VM.""" - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - - # Specify the VF/device by either PCI ID, index, or ethX+VFn - pciid = None - index = None - vfdisplay = "" - if "index" in args: - index = int(args["index"]) - if "pciid" in args: - pciid = args["pciid"] - vfdisplay = pciid - if "ethdev" in args and "vf" in args and not genericpci: - ethdev = args["ethdev"] - vfnum = args["vf"] - vfdisplay = "%s VF %s" % (ethdev, vfnum) - vfs = _get_vfs() - for vfpciid in vfs.keys(): - if (vfnum, ethdev) == vfs[vfpciid]: - pciid = vfpciid - break - if not pciid: - raise "Unable to find PCI ID for " + vfdisplay - - if not pciid and index == None: - if genericpci: - raise "Need to specify either a pciid or index" - else: - raise "Need to specify either a pciid, index or ethdev and vf" - - # Current assignments - current = _get_vm_assignments(session, vmuuid) - - # Remove the specified ID - if pciid: - if not pciid in [x[0] for x in current.values()]: - raise "VM %s does not have %s assigned" % (vmuuid, vfdisplay) - for i in current.keys(): - if current[i][0] == pciid: - del current[i] - else: - if index not in current: - raise "VM %s does not have PCI passthrough index %u" % (index) - del current[index] - - # Update the config - _set_vm_assignments(session, vmuuid, current) - return "" - -def assign_free_pci_device(session, args): - """Assign a free PCI device on this host to the specified VM.""" - assigned = _get_assignments(session) - if "vendorid" not in args: - raise "No vendor ID specified, please use the 'vendorid' argument" - if "deviceid" not in args: - raise "No device ID specified, please use the 'deviceid' argument" - # _get_devices will check syntax of the args - vendorid = args["vendorid"] - deviceid = args["deviceid"] - devices = _get_devices(vendorid, deviceid) - - for pciid in assigned.keys(): - if pciid in devices: - del devices[pciid] - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - - # If the caller specified a pass through index use that otherwise - # find the first one not currently configured. - ourassignments = _get_vm_assignments(session, vmuuid) - if "index" in args: - index = int(args["index"]) - else: - i = 0 - while True: - if not i in ourassignments.keys(): - index = i - break - i = i + 1 - - # Choose a suitable device. Preference is for lower numbered PCIIDs - pciids = devices.keys() - pciids.sort() - myid = None - if len(pciids) > 0: - myid = pciids[0] - if not myid: - raise "No spare %s:%s device" % (vendorid, deviceid) - - # Set up the config for the VM. No need to fill out the vendor and - # device id fields for this. - ourassignments[index] = (myid, None, None, None, None) - _set_vm_assignments(session, vmuuid, ourassignments) - - dom = xml.dom.minidom.Document() - element = dom.createElement("iovirt") - dom.appendChild(element) - - entry = dom.createElement("pcidevice") - element.appendChild(entry) - - subentry = dom.createElement("pciid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(myid)) - - subentry = dom.createElement("vendorid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vendorid)) - - subentry = dom.createElement("deviceid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(deviceid)) - - aentry = dom.createElement("assigned") - entry.appendChild(aentry) - - subentry = dom.createElement("vm") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vmuuid)) - - subentry = dom.createElement("index") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(str(index))) - - return dom.toprettyxml() - -def unassign_pci_device(session, args): - return unassign_vf(session, args, genericpci=True) - -def show_summary(session, args): - """Return a textual summary of SR-IOV configuarion of this host.""" - assigned = _get_assignments(session) - vfs = _get_vfs() - vfids = vfs.keys() - vfids.sort() - dom = xml.dom.minidom.Document() - element = dom.createElement("iovirt") - dom.appendChild(element) - - for vfid in vfids: - entry = dom.createElement("vf") - element.appendChild(entry) - - vfnum, vfeth = vfs[vfid] - - subentry = dom.createElement("pciid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfid)) - - subentry = dom.createElement("device") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfeth)) - - subentry = dom.createElement("vfnum") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfnum)) - - if vfid in assigned: - vmuuid, vmnum, vendorid, deviceid, mac, vlan = assigned[vfid] - aentry = dom.createElement("assigned") - entry.appendChild(aentry) - - subentry = dom.createElement("vm") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vmuuid)) - - subentry = dom.createElement("index") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(str(vmnum))) - - # MAC and VLAN go in the parent node - if mac: - subentry = dom.createElement("mac") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(mac)) - if vlan: - subentry = dom.createElement("vlan") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vlan)) - - return dom.toprettyxml() - -def list_pci_devices(session, args): - """Return a list of PCI devices of the specified vendorid:deviceid and their assignments.""" - if "vendorid" not in args: - raise "No vendor ID specified, please use the 'vendorid' argument" - if "deviceid" not in args: - raise "No device ID specified, please use the 'deviceid' argument" - # _get_devices will check syntax of the args - vendorid = args["vendorid"] - deviceid = args["deviceid"] - devices = _get_devices(vendorid, deviceid) - assigned = _get_assignments(session) - pciids = devices.keys() - pciids.sort() - dom = xml.dom.minidom.Document() - element = dom.createElement("iovirt") - dom.appendChild(element) - for pciid in pciids: - entry = dom.createElement("pcidevice") - element.appendChild(entry) - - subentry = dom.createElement("pciid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(pciid)) - - subentry = dom.createElement("vendorid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vendorid)) - - subentry = dom.createElement("deviceid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(deviceid)) - - if pciid in assigned: - vmuuid, vmnum, vendorid, deviceid, mac, vlan = assigned[pciid] - aentry = dom.createElement("assigned") - entry.appendChild(aentry) - - subentry = dom.createElement("vm") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vmuuid)) - - subentry = dom.createElement("index") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(str(vmnum))) - - if mac: - subentry = dom.createElement("mac") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(mac)) - if vlan: - subentry = dom.createElement("vlan") - aentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vlan)) - - return dom.toprettyxml() - -def get_vm(session, args): - """Return a description of the SR-IOV config for the specified VM.""" - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - vfs = _get_vfs() - current = _get_vm_assignments(session, vmuuid) - indexlist = current.keys() - indexlist.sort() - dom = xml.dom.minidom.Document() - element = dom.createElement("iovirt") - dom.appendChild(element) - vmentry = dom.createElement("vm") - element.appendChild(vmentry) - subentry = dom.createElement("uuid") - vmentry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vmuuid)) - for i in indexlist: - entry = dom.createElement("passthrough") - vmentry.appendChild(entry) - subentry = dom.createElement("index") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(str(i))) - - pciid, vendorid, deviceid, mac, vlan = current[i] - - subentry = dom.createElement("pciid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(pciid)) - subentry = dom.createElement("vendorid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vendorid)) - subentry = dom.createElement("deviceid") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(deviceid)) - if mac: - subentry = dom.createElement("mac") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(mac)) - if vlan: - subentry = dom.createElement("vlan") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vlan)) - if pciid in vfs: - vfnum, ethdev = vfs[pciid] - subentry = dom.createElement("vfnum") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(vfnum)) - subentry = dom.createElement("device") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode(ethdev)) - subentry = dom.createElement("pttype") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode("vf")) - else: - subentry = dom.createElement("pttype") - entry.appendChild(subentry) - subentry.appendChild(dom.createTextNode("pcidevice")) - return dom.toprettyxml() - -def prep_for_vm(session, args): - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - current = _get_vm_assignments(session, vmuuid) - vfs = _get_vfs() - reply = [] - for i in current.keys(): - pciid, vendorid, deviceid, mac, vlan = current[i] - if not pciid in vfs.keys(): - # This is probably a non SR-IOV PCI device being passed - # through. Log that we cannot find details and carry on - # processing the device list. - syslog.syslog("Could not find VF details for %s for %s, assume it is a non SR-IOV passthrough" % (pciid, vmuuid)) - continue - vfnum, ethdev = vfs[pciid] - if mac: - # Check MAC really is a MAC - if not re.match("^([0-9a-fA-F:]+)$", mac): - raise "Unexpected MAC text '%s' for VM %s" % (mac, vmuuid) - cmd = "%s link set %s vf %s mac %s" % (ipcmd, ethdev, vfnum, mac) - reply.append(cmd) - syslog.syslog("Setting VF MAC with '%s' for VM %s" % (cmd, vmuuid)) - os.system(cmd) - if not vlan: - # No VLAN may need explicit clearly of previously assigned VLAN - vlan = "0" - # Check VLAN really is an integer - if not re.match("^\d+$", vlan): - raise "Unexpected VLAN text '%s' for VM %s" % (vlan, vmuuid) - cmd2 = "%s link set %s vf %s vlan %s" % (ipcmd, ethdev, vfnum, vlan) - reply.append(cmd2) - syslog.syslog("Setting VF VLAN with '%s' for VM %s" % (cmd, vmuuid)) - os.system(cmd2) - return "\n".join(reply) - -def unassign_all(session, args): - """Clear all SR-IOV and PCI passthrough devices from a VM.""" - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - - # Update the config - _set_vm_assignments(session, vmuuid, {}) - return "" - -def change_vf_mac(session, args): - """Change the MAC address for a VF already assigned to a VM.""" - - if "mac" not in args: - raise "Need to specify a new MAC address" - mac = args["mac"] - - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - - # Specify the VF by index - if "index" not in args: - raise "Need to specify the VF index" - index = int(args["index"]) - - # Current assignments - current = _get_vm_assignments(session, vmuuid) - - # Update the MAC - if index not in current: - raise "VF index %u not found for VM %s" % (index, vmuuid) - c = current[index] - current[index] = (c[0], c[1], c[2], mac, c[4]) - - # Update the config - _set_vm_assignments(session, vmuuid, current) - - return "" - -def change_vf_vlan(session, args): - """Change the VLAN for a VF already assigned to a VM. - Use vlan=None to remove VLAN tagging.""" - - if "vlan" not in args: - raise "Need to specify a new VLAN" - vlan = args["vlan"] - if vlan.lower() == "none": - vlan = None - - if "uuid" not in args: - raise "No VM UUID specified, please use the 'uuid' argument" - vmuuid = args["uuid"] - - # Specify the VF by index - if "index" not in args: - raise "Need to specify the VF index" - index = int(args["index"]) - - # Current assignments - current = _get_vm_assignments(session, vmuuid) - - # Update the MAC - if index not in current: - raise "VF index %u not found for VM %s" % (index, vmuuid) - c = current[index] - current[index] = (c[0], c[1], c[2], c[3], vlan) - - # Update the config - _set_vm_assignments(session, vmuuid, current) - - return "" - -if __name__ == "__main__": - XenAPIPlugin.dispatch({"enable_iommu": enable_iommu, - "assign_free_vf": assign_free_vf, - "show_summary": show_summary, - "unassign_vf": unassign_vf, - "assign_free_pci_device": assign_free_pci_device, - "unassign_pci_device": unassign_pci_device, - "get_vm": get_vm, - "prep_for_vm": prep_for_vm, - "list_pci_devices": list_pci_devices, - "unassign_all": unassign_all, - "change_vf_vlan": change_vf_vlan, - "change_vf_mac": change_vf_mac}) - From 6e9a66a1e63b46cb68f25c8a4efb04afb6a3e815 Mon Sep 17 00:00:00 2001 From: Colin James Date: Wed, 5 Jun 2024 15:28:37 +0100 Subject: [PATCH 83/99] CP-49129: Add unit test for parallel parsing. Initial commit message: "Provides simple code to generate S-expression trees and attempt to parse them across a large number of threads. It serves to demonstrate that ocamlyacc's generated parses are not thread-safe (in that they modify a global variable "env")." Move Colin's unit test for parallel parsing into xapi. Signed-off-by: Colin Barr Signed-off-by: Gabriel Buica --- ocaml/libs/sexpr/dune | 3 +- ocaml/libs/sexpr/test/dune | 4 ++ ocaml/libs/sexpr/test/test_sexpr.ml | 87 ++++++++++++++++++++++++++++ ocaml/libs/sexpr/test/test_sexpr.mli | 0 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 ocaml/libs/sexpr/test/dune create mode 100644 ocaml/libs/sexpr/test/test_sexpr.ml create mode 100644 ocaml/libs/sexpr/test/test_sexpr.mli diff --git a/ocaml/libs/sexpr/dune b/ocaml/libs/sexpr/dune index 97965164eed..b75fdc64c0f 100644 --- a/ocaml/libs/sexpr/dune +++ b/ocaml/libs/sexpr/dune @@ -17,9 +17,10 @@ (executable (modes exe) (name sexprpp) + (public_name sexprpp) + (package sexpr) (modules sexprpp) (libraries sexpr ) ) - diff --git a/ocaml/libs/sexpr/test/dune b/ocaml/libs/sexpr/test/dune new file mode 100644 index 00000000000..43a5a9f5b66 --- /dev/null +++ b/ocaml/libs/sexpr/test/dune @@ -0,0 +1,4 @@ +(test + (name test_sexpr) + (modules test_sexpr) + (libraries sexpr astring rresult qcheck threads)) diff --git a/ocaml/libs/sexpr/test/test_sexpr.ml b/ocaml/libs/sexpr/test/test_sexpr.ml new file mode 100644 index 00000000000..67b33f3f197 --- /dev/null +++ b/ocaml/libs/sexpr/test/test_sexpr.ml @@ -0,0 +1,87 @@ +(* + * Copyright (C) 2024 Cloud Software Group + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; version 2.1 only. with the special + * exception on linking described in file LICENSE. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +module Tree = struct + type 'a t = Node of 'a * 'a t list + + let rec show pv = function + | Node (l, []) -> + pv l + | Node (l, xs) -> + let xs = String.concat " " List.(map (show pv) xs) in + Printf.sprintf "(%s %s)" (pv l) xs +end + +module Test = struct + let labels = [|"foo"; "bar"; "baz"|] + + let label_gen = + QCheck.Gen.(map (Array.get labels) (int_bound (Array.length labels - 1))) + + let tree_gen = + let open QCheck.Gen in + let node l xs = Tree.Node (l, xs) in + let lo, hi = (2, 6) in + let go recur = function + | 0 -> + map2 node label_gen (return []) + | depth -> + let xs_gen = list_size (int_range 2 4) (recur (depth - 1)) in + map2 node label_gen xs_gen + in + sized_size (int_range lo hi) (fix go) + + type 'a outcome = + | Unfinished + | Finished of 'a + | Excepted of exn * Printexc.raw_backtrace + + let is_exceptional = function Excepted _ -> true | _ -> false + + let go n = + Printexc.record_backtrace true ; + let outcomes = Array.init n (Fun.const Unfinished) in + let trees = QCheck.Gen.generate ~n tree_gen in + let parse (i, input) = + (* Continually parse input until ~200ms has elapsed. *) + let go () = + let start = Unix.gettimeofday () in + while Unix.gettimeofday () -. start < 0.2 do + ignore (SExpr_TS.of_string input) + done + in + (* Trap any exception and populate outcomes with it. *) + let open Rresult.R in + match trap_exn go () with + | Ok () -> + outcomes.(i) <- Finished () + | Error (`Exn_trap (exn, trace)) -> + outcomes.(i) <- Excepted (exn, trace) + in + let tids = + let launch (tids, i) tree = + let tid = Thread.create parse (i, Tree.show Fun.id tree) in + (tid :: tids, i + 1) + in + fst (List.fold_left launch ([], 0) trees) + in + List.iter Thread.join tids ; + match Array.find_opt is_exceptional outcomes with + | Some (Excepted (_, trace)) -> + Printexc.print_raw_backtrace Out_channel.stdout trace + | _ -> + () +end + +let () = Test.go 10 diff --git a/ocaml/libs/sexpr/test/test_sexpr.mli b/ocaml/libs/sexpr/test/test_sexpr.mli new file mode 100644 index 00000000000..e69de29bb2d From 16fcea830afffe770e8ac162e99dd2460b04ea4a Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Tue, 11 Jun 2024 15:38:41 +0100 Subject: [PATCH 84/99] CP-49129: Make unit test run on alcotest. Make `test_sexpr` run using alcotest. Signed-off-by: Gabriel Buica --- ocaml/libs/sexpr/test/dune | 2 +- ocaml/libs/sexpr/test/test_sexpr.ml | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/ocaml/libs/sexpr/test/dune b/ocaml/libs/sexpr/test/dune index 43a5a9f5b66..46433eddf97 100644 --- a/ocaml/libs/sexpr/test/dune +++ b/ocaml/libs/sexpr/test/dune @@ -1,4 +1,4 @@ (test (name test_sexpr) (modules test_sexpr) - (libraries sexpr astring rresult qcheck threads)) + (libraries sexpr astring rresult qcheck alcotest threads)) diff --git a/ocaml/libs/sexpr/test/test_sexpr.ml b/ocaml/libs/sexpr/test/test_sexpr.ml index 67b33f3f197..80d7755cb49 100644 --- a/ocaml/libs/sexpr/test/test_sexpr.ml +++ b/ocaml/libs/sexpr/test/test_sexpr.ml @@ -30,6 +30,7 @@ module Test = struct QCheck.Gen.(map (Array.get labels) (int_bound (Array.length labels - 1))) let tree_gen = + (*Alternatively, this can use QCheck.Gen.fix.*) let open QCheck.Gen in let node l xs = Tree.Node (l, xs) in let lo, hi = (2, 6) in @@ -49,6 +50,21 @@ module Test = struct let is_exceptional = function Excepted _ -> true | _ -> false + let assert_exceptional outcomes = + outcomes + |> Array.iter (fun outcome -> + let exceptional = is_exceptional outcome in + let msg = + match (exceptional, outcome) with + | true, Excepted (_, trace) -> + Printf.sprintf "Exception found when parsing Sexpr: %s" + (Printexc.raw_backtrace_to_string trace) + | _ -> + "" + in + Alcotest.(check bool) msg false exceptional + ) + let go n = Printexc.record_backtrace true ; let outcomes = Array.init n (Fun.const Unfinished) in @@ -77,11 +93,11 @@ module Test = struct fst (List.fold_left launch ([], 0) trees) in List.iter Thread.join tids ; - match Array.find_opt is_exceptional outcomes with - | Some (Excepted (_, trace)) -> - Printexc.print_raw_backtrace Out_channel.stdout trace - | _ -> - () + assert_exceptional outcomes end -let () = Test.go 10 +let test_parsing () = Test.go 10 + +let test = [("Parallel Parsing", `Quick, test_parsing)] + +let () = Alcotest.run "Sexpr parser" [("parallel parsing", test)] From 9f50af8344fb6fb2ed3c4c0aeac8fe08fa16980d Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Thu, 6 Jun 2024 10:46:10 +0100 Subject: [PATCH 85/99] CP-49129: Replace `ocamlyacc` with `menhir` From https://ocaml.org/manual/5.1/lexyacc.html on ocamlyacc concurrency sefety: "Parsers generated by ocamlyacc are not thread-safe. Those parsers rely on an internal work state which is shared by all ocamlyacc generated parsers. The menhir parser generator is a better option if you want thread-safe parsers." We currently hold a global lock while calling the sexpr parser, so even if we try to parse a small sexpression, it'll be blocked behind parsing the large one. Switch to Menhir to make this thread-safe. Signed-off-by: Gabriel Buica --- dune-project | 1 + ocaml/libs/sexpr/dune | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dune-project b/dune-project index ac02a1c296d..3078c5af641 100644 --- a/dune-project +++ b/dune-project @@ -1,5 +1,6 @@ (lang dune 3.0) (formatting (enabled_for ocaml)) +(using menhir 2.0) (generate_opam_files true) diff --git a/ocaml/libs/sexpr/dune b/ocaml/libs/sexpr/dune index b75fdc64c0f..02fa8cf9c7f 100644 --- a/ocaml/libs/sexpr/dune +++ b/ocaml/libs/sexpr/dune @@ -1,4 +1,4 @@ -(ocamlyacc sExprParser) +(menhir (modules sExprParser)) (ocamllex sExprLexer) From 8e21bd8c9c5f40166323366440dd6724bacf8b86 Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Wed, 5 Jun 2024 15:38:19 +0100 Subject: [PATCH 86/99] CP-49129: Drop global lock around sexpr parsing Using `ocamlyacc` with this change results in a test failure. Thus showing the issue of parallel parsing using `ocamlyacc`. Signed-off-by: Gabriel Buica --- ocaml/libs/sexpr/sExpr_TS.ml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ocaml/libs/sexpr/sExpr_TS.ml b/ocaml/libs/sexpr/sExpr_TS.ml index f91fda10413..e68c7d677a8 100644 --- a/ocaml/libs/sexpr/sExpr_TS.ml +++ b/ocaml/libs/sexpr/sExpr_TS.ml @@ -12,11 +12,6 @@ * GNU Lesser General Public License for more details. *) -let lock = Mutex.create () - -let of_string s = - Xapi_stdext_threads.Threadext.Mutex.execute lock (fun () -> - SExprParser.expr SExprLexer.token (Lexing.from_string s) - ) +let of_string s = SExprParser.expr SExprLexer.token (Lexing.from_string s) let string_of = SExpr.string_of From 59e2371d41b6d21a68612e85e21b7dd03477c0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Mon, 29 Apr 2024 18:22:05 +0100 Subject: [PATCH 87/99] CP-49045: replace all uses of ocamlyacc with menhir which is thread-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edwin Török --- ocaml/database/dune | 2 +- ocaml/xenopsd/cli/dune | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ocaml/database/dune b/ocaml/database/dune index e135f3d7e63..971385cb49e 100644 --- a/ocaml/database/dune +++ b/ocaml/database/dune @@ -1,6 +1,6 @@ (ocamllex db_filter_lex) -(ocamlyacc db_filter_parse) +(menhir (modules db_filter_parse)) (library (name xapi_schema) diff --git a/ocaml/xenopsd/cli/dune b/ocaml/xenopsd/cli/dune index 220d5aae2e2..b194b10323c 100644 --- a/ocaml/xenopsd/cli/dune +++ b/ocaml/xenopsd/cli/dune @@ -1,4 +1,4 @@ -(ocamlyacc xn_cfg_parser) +(menhir (modules xn_cfg_parser)) (ocamllex xn_cfg_lexer) (executable From d6d5c4dd9c999e0f98b9454f0c6362ed42569058 Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Wed, 12 Jun 2024 10:33:16 +0100 Subject: [PATCH 88/99] CP-49129: Update `quality-gate.sh` for `ocamlyacc` We want to be sure there are no more uses of `ocamlyacc`, because of concurrency issues. Menhir should be used instead. Signed-off-by: Gabriel Buica --- quality-gate.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/quality-gate.sh b/quality-gate.sh index c723676d1a5..b49c12ecc28 100755 --- a/quality-gate.sh +++ b/quality-gate.sh @@ -82,10 +82,22 @@ vtpm-fields () { esac } +ocamlyacc () { + N=0 + OCAMLYACC=$(git grep -r -o --count "ocamlyacc" '**/dune' | wc -l) + if [ "$OCAMLYACC" -eq "$N" ]; then + echo "OK found $OCAMLYACC usages of ocamlyacc usages in dune files." + else + echo "ERROR expected $N usages of ocamlyacc in dune files, got $OCAMLYACC." 1>&2 + exit 1 + fi +} + list-hd verify-cert mli-files structural-equality vtpm-unimplemented vtpm-fields +ocamlyacc From 690a53aa70cfd6c9f12b0055583c1f63c4486ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Wed, 12 Jun 2024 15:25:15 +0100 Subject: [PATCH 89/99] Link just qcheck-core, not qcheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qcheck is in the wrong part of xs-opam currently, and although we're moving it this test really only needs qcheck-core, so link only that to fix an internal CI failure. Signed-off-by: Edwin Török --- ocaml/libs/sexpr/test/dune | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocaml/libs/sexpr/test/dune b/ocaml/libs/sexpr/test/dune index 46433eddf97..2e60f3cd9b6 100644 --- a/ocaml/libs/sexpr/test/dune +++ b/ocaml/libs/sexpr/test/dune @@ -1,4 +1,4 @@ (test (name test_sexpr) (modules test_sexpr) - (libraries sexpr astring rresult qcheck alcotest threads)) + (libraries sexpr astring rresult qcheck-core alcotest threads)) From d5f182541d9a465af2a142fdd1969b13e5193e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Wed, 12 Jun 2024 15:32:36 +0100 Subject: [PATCH 90/99] Define qcheck-core dependency in opam packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edwin Török --- sexpr.opam.template | 1 + wsproxy.opam | 2 +- wsproxy.opam.template | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sexpr.opam.template b/sexpr.opam.template index 0448d5c4d16..d83e0f2a493 100644 --- a/sexpr.opam.template +++ b/sexpr.opam.template @@ -11,6 +11,7 @@ depends: [ "ocaml" "dune" "astring" + "qcheck-core" {with-test} "xapi-stdext-threads" ] synopsis: "Library required by xapi" diff --git a/wsproxy.opam b/wsproxy.opam index baceb5d55fe..d0ee062cf1c 100644 --- a/wsproxy.opam +++ b/wsproxy.opam @@ -22,7 +22,7 @@ depends: [ "re" "uuid" "ounit2" {with-test} - "qcheck" {with-test} + "qcheck-core" {with-test} ] tags: [ "org:xapi-project" ] synopsis: "Websockets proxy for VNC traffic" diff --git a/wsproxy.opam.template b/wsproxy.opam.template index 5a5fdd00975..483d486d4c0 100644 --- a/wsproxy.opam.template +++ b/wsproxy.opam.template @@ -20,7 +20,7 @@ depends: [ "re" "uuid" "ounit2" {with-test} - "qcheck" {with-test} + "qcheck-core" {with-test} ] tags: [ "org:xapi-project" ] synopsis: "Websockets proxy for VNC traffic" From 32d5243a426bc09e00819bc025717c15f89aa81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Thu, 13 Jun 2024 15:37:54 +0100 Subject: [PATCH 91/99] Makefile: fix compatibility with the dash shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Github CI runs the makefile using SHELL=/bin/dash, which doesn't support SIG* names for traps. Drop the SIG prefix, which works with both `dash` and `bash`. Signed-off-by: Edwin Török --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 38c7545cc83..b2198bd5eae 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ test: ulimit -S -t $(TEST_TIMEOUT); \ (sleep $(TEST_TIMEOUT) && ps -ewwlyF --forest)& \ PSTREE_SLEEP_PID=$$!; \ - trap "kill $${PSTREE_SLEEP_PID}" SIGINT SIGTERM EXIT; \ + trap "kill $${PSTREE_SLEEP_PID}" INT TERM EXIT; \ timeout --foreground $(TEST_TIMEOUT2) \ dune runtest --profile=$(PROFILE) --error-reporting=twice -j $(JOBS) ifneq ($(PY_TEST), NO) From 0bc1a1222c74d0c8d0cba74b5e927433f28da6aa Mon Sep 17 00:00:00 2001 From: Ming Lu Date: Wed, 12 Jun 2024 14:52:09 +0800 Subject: [PATCH 92/99] CP-49858: Fix phrasing in readme Signed-off-by: Ming Lu --- ocaml/sdk-gen/go/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ocaml/sdk-gen/go/README.md b/ocaml/sdk-gen/go/README.md index aa173a6b5d1..2cf13fee41b 100644 --- a/ocaml/sdk-gen/go/README.md +++ b/ocaml/sdk-gen/go/README.md @@ -48,12 +48,12 @@ Extract the contents of this archive. A. Navigate to the extracted `XenServer-SDK\XenServerGo\src` directory and copy the whole folder `src` into your Go project directory. -B. To use XenServer Go SDK as a local Go module, update one line into the `go.mod` file under your Go project: +B. To use the XenServer SDK for Go as a local Go module, include the following line into the `go.mod` file under your Go project: ``` replace xenapi => ./src ``` -You can then import this XenServer SDK Go module with the following command: +You can then import the XenServer SDK for Go with the following command: ``` import "xenapi" From 5eb7889ec9a5f47d0f461b8d1bdc013a4315ea7c Mon Sep 17 00:00:00 2001 From: Ming Lu Date: Wed, 12 Jun 2024 16:02:07 +0800 Subject: [PATCH 93/99] CP-49858: Add licence text on top of Go source files Signed-off-by: Ming Lu --- .../sdk-gen/go/autogen/src/jsonrpc_client.go | 29 +++++++++++++++++++ ocaml/sdk-gen/go/gen_go_binding.ml | 5 +++- ocaml/sdk-gen/go/gen_go_helper.ml | 3 ++ ocaml/sdk-gen/go/gen_go_helper.mli | 2 ++ .../sdk-gen/go/templates/FileHeader.mustache | 4 ++- 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go index 09600a5bd68..ec61b1bb987 100644 --- a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -1,3 +1,32 @@ +/* + * Copyright (c) Cloud Software Group, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package xenapi import ( diff --git a/ocaml/sdk-gen/go/gen_go_binding.ml b/ocaml/sdk-gen/go/gen_go_binding.ml index 0203e16966c..eb7bc73a96b 100644 --- a/ocaml/sdk-gen/go/gen_go_binding.ml +++ b/ocaml/sdk-gen/go/gen_go_binding.ml @@ -17,7 +17,7 @@ open Gen_go_helper let render_enums enums destdir = let header = render_template "FileHeader.mustache" - (`O [("modules", `Null)]) + (`O [("modules", `Null); licence]) ~newline:true () in let enums = render_template "Enum.mustache" enums () |> String.trim in @@ -36,6 +36,7 @@ let render_api_versions destdir = ; ("items", `A (List.map name ["errors"; "fmt"])) ] ) + ; licence ] in let rendered = @@ -55,6 +56,7 @@ let render_api_messages_and_errors destdir = ("api_errors", `A Json.api_errors) ; ("api_messages", `A Json.api_messages) ; ("modules", `Null) + ; licence ] in let header = render_template "FileHeader.mustache" obj ~newline:true () in @@ -83,6 +85,7 @@ let render_convert_header () = ) ] ) + ; licence ] in render_template "FileHeader.mustache" obj ~newline:true () diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index cefaaad854a..3e50c39189e 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -22,6 +22,8 @@ let templates_dir = "templates" let ( // ) = Filename.concat +let licence = ("licence", `String Licence.bsd_two_clause) + let acronyms = [ "id" @@ -479,6 +481,7 @@ module Json = struct ; ("modules", modules) ; ("messages", `A (messages_of_obj obj)) ; ("option", `A (of_options types)) + ; licence ] in let assoc_list = base_assoc_list @ get_event_session_value obj.name in diff --git a/ocaml/sdk-gen/go/gen_go_helper.mli b/ocaml/sdk-gen/go/gen_go_helper.mli index 623af766361..d7ba644bdf9 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.mli +++ b/ocaml/sdk-gen/go/gen_go_helper.mli @@ -13,6 +13,8 @@ val ( // ) : string -> string -> string +val licence : string * Mustache.Json.value + val snake_to_camel : ?internal:bool -> string -> string val render_template : diff --git a/ocaml/sdk-gen/go/templates/FileHeader.mustache b/ocaml/sdk-gen/go/templates/FileHeader.mustache index a21900ec1c9..de563563d0e 100644 --- a/ocaml/sdk-gen/go/templates/FileHeader.mustache +++ b/ocaml/sdk-gen/go/templates/FileHeader.mustache @@ -1,3 +1,5 @@ +{{{licence}}} + package xenapi {{#modules}} {{#import}} @@ -8,4 +10,4 @@ import ( {{/items}} ) {{/import}} -{{/modules}} \ No newline at end of file +{{/modules}} From 50cbe0fb93e012a6ac976d5ef74449974cda9f02 Mon Sep 17 00:00:00 2001 From: Ming Lu Date: Wed, 12 Jun 2024 19:55:35 +0800 Subject: [PATCH 94/99] CP-49858: Unit test: licence template variable Signed-off-by: Ming Lu --- ocaml/sdk-gen/go/test_data/file_header.go | 29 +++++++++++++++++++++++ ocaml/sdk-gen/go/test_gen_go.ml | 1 + 2 files changed, 30 insertions(+) diff --git a/ocaml/sdk-gen/go/test_data/file_header.go b/ocaml/sdk-gen/go/test_data/file_header.go index a40c01a5eb8..cd7cdeb7f7f 100644 --- a/ocaml/sdk-gen/go/test_data/file_header.go +++ b/ocaml/sdk-gen/go/test_data/file_header.go @@ -1,3 +1,32 @@ +/* + * Copyright (c) Cloud Software Group, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1) Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package xenapi import ( diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index 4c06f17d28f..ff83521620f 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -592,6 +592,7 @@ let header : Mustache.Json.t = ) ] ) + ; ("licence", `String Licence.bsd_two_clause) ] let enums : Mustache.Json.t = From 7ac6afa7e0766c3be6c2b7a3d703cd04716e2e55 Mon Sep 17 00:00:00 2001 From: Ming Lu Date: Wed, 12 Jun 2024 19:12:07 +0800 Subject: [PATCH 95/99] CP-49858: Remove template variables 'first' and 'is_session_id' Prior to this change, the function parameter list is generated by Mustache template. As a result, a template boolean variable 'first' has to be used to determine if a leading comma is presented or not. E.g. when the 'first' is true, a parameter would be rendered as "a string', and then the next parameter with the 'first' being false would be rendered as ", b string". Putting them together would result in "a string, b string". This could work but is difficult to be understood. In this commit, it is changed to construct the whole function parameter list as a string in OCaml code and bind it to the template variable 'func_params'. Similar changes apply to the template variable 'is_session_id'. It is removed in this commit and the expected values are generated by OCaml code. Signed-off-by: Ming Lu --- ocaml/sdk-gen/go/gen_go_helper.ml | 148 ++++++++++++------ .../sdk-gen/go/templates/FileHeader.mustache | 1 - ocaml/sdk-gen/go/templates/Methods.mustache | 10 +- .../go/templates/SessionMethod.mustache | 10 +- 4 files changed, 110 insertions(+), 59 deletions(-) diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 3e50c39189e..47540f55ef7 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -318,38 +318,6 @@ module Json = struct | [] -> failwith "Empty params group should not exist." - let of_param param = - let name_internal name = - let name = snake_to_camel ~internal:true name in - match name with "type" -> "typeKey" | "interface" -> "inter" | _ -> name - in - let suffix_of_type = suffix_of_type param.param_type in - let t, _e = string_of_ty_with_enums param.param_type in - let name = param.param_name in - [ - ("is_session_id", `Bool (name = "session_id")) - ; ("type", `String t) - ; ("name", `String name) - ; ("name_internal", `String (name_internal name)) - ; ("doc", `String param.param_doc) - ; ("func_name_suffix", `String suffix_of_type) - ] - - (* We use ',' to seprate params in Go function, we should ignore ',' before first param, - for example `func(a type1, b type2)` is wanted rather than `func(, a type1, b type2)`. - *) - let of_params = function - | head :: rest -> - let head = `O (("first", `Bool true) :: of_param head) in - let rest = - List.map - (fun item -> `O (("first", `Bool false) :: of_param item)) - rest - in - head :: rest - | [] -> - [] - let of_error e = `O [("name", `String e.err_name); ("doc", `String e.err_doc)] let of_errors = function @@ -393,10 +361,81 @@ module Json = struct else method_name ^ string_of_int (List.length params) - (* Since the param of `session *Session` isn't needed in functions of session object, - we add a special "func_params" field for session object to ignore `session *Session`.*) - let addtion_info_of_session method_name params latest = - let add_session_info method_name = + module ParamInfo = struct + type param_info = { + typ: string + ; func_param: string + ; name: string + ; name_internal: string + ; name_in_serialize_call: string + ; doc: string + ; func_name_suffix: string + } + + let to_param_info ~is_sess_func param = + let suffix_of_type = suffix_of_type param.param_type in + let t, _e = string_of_ty_with_enums param.param_type in + let name = param.param_name in + let is_session_id = name = "session_id" in + let internal_name = + let name' = snake_to_camel ~internal:true name in + match name' with + | "type" -> + "typeKey" + | "interface" -> + "inter" + | _ -> + name' + in + let func_param = + match (is_sess_func, is_session_id) with + | true, _ | false, false -> + Printf.sprintf "%s %s" internal_name t + | false, true -> + "session *Session" + in + let name_in_serialize_call = + match (is_sess_func, is_session_id) with + | false, false | true, false -> + internal_name + | true, true -> + "class.ref" + | false, true -> + "session.ref" + in + { + typ= t + ; func_param + ; name + ; name_internal= internal_name + ; name_in_serialize_call + ; doc= param.param_doc + ; func_name_suffix= suffix_of_type + } + end + + let of_param_info param_info = + let open ParamInfo in + let p = param_info in + `O + [ + ("type", `String p.typ) + ; ("name", `String p.name) + ; ("name_internal", `String p.name_internal) + ; ("name_in_serialize_call", `String p.name_in_serialize_call) + ; ("doc", `String p.doc) + ; ("func_name_suffix", `String p.func_name_suffix) + ] + + let of_param ~is_sess_func param = + ParamInfo.to_param_info ~is_sess_func param |> of_param_info + + let to_func_params params = + List.map (fun p -> p.ParamInfo.func_param) params |> String.concat ", " + + let diverse_info_of_session method_name params msg_session latest = + let is_sess_func = true in + let session_info method_name = match method_name with | "login_with_password" | "slave_local_login_with_password" -> [("session_login", `Bool true); ("session_logout", `Bool false)] @@ -405,21 +444,35 @@ module Json = struct | _ -> [("session_login", `Bool false); ("session_logout", `Bool false)] in - let name = method_name_exported method_name params latest in - ("func_params", `A (of_params params)) + (* In a session function, the session is from the instance rather than parameter list *) + let params' = if msg_session then List.tl params else params in + let name = method_name_exported method_name params' latest in + let func_params = + List.map (ParamInfo.to_param_info ~is_sess_func) params' |> to_func_params + in + ("func_params", `String func_params) + :: ("params", `A (List.map (of_param ~is_sess_func) params)) :: ("method_name_exported", `String name) - :: add_session_info method_name + :: session_info method_name - let addtion_info msg params latest = + (* This is for the variables which are diverse for different messages *) + let diverse_info msg params latest = let method_name = msg.msg_name in - match (String.lowercase_ascii msg.msg_obj_name, msg.msg_session) with - | "session", true -> - addtion_info_of_session method_name (List.tl params) latest - | "session", false -> - addtion_info_of_session method_name params latest + match String.lowercase_ascii msg.msg_obj_name with + | "session" -> + diverse_info_of_session method_name params msg.msg_session latest | _ -> + let is_sess_func = false in + let func_params = + List.map (ParamInfo.to_param_info ~is_sess_func) params + |> to_func_params + in let name = method_name_exported method_name params latest in - [("method_name_exported", `String name)] + [ + ("func_params", `String func_params) + ; ("params", `A (List.map (of_param ~is_sess_func) params)) + ; ("method_name_exported", `String name) + ] let messages_of_obj obj = let ctor_fields = ctor_fields_of_obj obj in @@ -433,14 +486,13 @@ module Json = struct ; ("class_name_exported", `String (snake_to_camel obj.name)) ; ("description", desc_of_msg msg ctor_fields) ; ("result", of_result obj msg) - ; ("params", `A (of_params params)) ; ("errors", of_errors msg.msg_errors) ; ("has_error", `Bool (msg.msg_errors <> [])) ; ("async", `Bool msg.msg_async) ; ("version", `String rel_version) ] in - `O (base_assoc_list @ addtion_info msg params latest) + `O (base_assoc_list @ diverse_info msg params latest) in msg |> group_params |> List.map of_message ) diff --git a/ocaml/sdk-gen/go/templates/FileHeader.mustache b/ocaml/sdk-gen/go/templates/FileHeader.mustache index de563563d0e..d45ecac751c 100644 --- a/ocaml/sdk-gen/go/templates/FileHeader.mustache +++ b/ocaml/sdk-gen/go/templates/FileHeader.mustache @@ -1,5 +1,4 @@ {{{licence}}} - package xenapi {{#modules}} {{#import}} diff --git a/ocaml/sdk-gen/go/templates/Methods.mustache b/ocaml/sdk-gen/go/templates/Methods.mustache index 384b6949499..3adbbb37834 100644 --- a/ocaml/sdk-gen/go/templates/Methods.mustache +++ b/ocaml/sdk-gen/go/templates/Methods.mustache @@ -8,10 +8,10 @@ {{#errors}} // {{name}} - {{doc}} {{/errors}} -func ({{name_internal}}) {{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) ({{#result}}retval {{type}}, {{/result}}err error) { +func ({{name_internal}}) {{method_name_exported}}({{func_params}}) ({{#result}}retval {{type}}, {{/result}}err error) { method := "{{class_name}}.{{method_name}}" {{#params}} - {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{name_in_serialize_call}}) if err != nil { return } @@ -36,10 +36,10 @@ func ({{name_internal}}) {{method_name_exported}}({{#params}}{{#first}}session * {{#errors}} // {{name}} - {{doc}} {{/errors}} -func ({{name_internal}}) Async{{method_name_exported}}({{#params}}{{#first}}session *Session{{/first}}{{^first}}, {{name_internal}} {{type}}{{/first}}{{/params}}) (retval TaskRef, err error) { +func ({{name_internal}}) Async{{method_name_exported}}({{func_params}}) (retval TaskRef, err error) { method := "Async.{{class_name}}.{{method_name}}" {{#params}} - {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#first}}session.ref{{/first}}{{^first}}{{name_internal}}{{/first}}) + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{name_in_serialize_call}}) if err != nil { return } @@ -53,4 +53,4 @@ func ({{name_internal}}) Async{{method_name_exported}}({{#params}}{{#first}}sess } {{/async}} -{{/messages}} \ No newline at end of file +{{/messages}} diff --git a/ocaml/sdk-gen/go/templates/SessionMethod.mustache b/ocaml/sdk-gen/go/templates/SessionMethod.mustache index 7613c69e75f..a186ec3ffcf 100644 --- a/ocaml/sdk-gen/go/templates/SessionMethod.mustache +++ b/ocaml/sdk-gen/go/templates/SessionMethod.mustache @@ -8,10 +8,10 @@ {{#errors}} // {{name}} - {{doc}} {{/errors}} -func (class *Session) {{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) ({{#result}}retval {{type}}, {{/result}}err error) { +func (class *Session) {{method_name_exported}}({{func_params}}) ({{#result}}retval {{type}}, {{/result}}err error) { method := "{{class_name}}.{{method_name}}" {{#params}} - {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{name_in_serialize_call}}) if err != nil { return } @@ -46,10 +46,10 @@ func (class *Session) {{method_name_exported}}({{#func_params}}{{^first}}, {{/fi {{#errors}} // {{name}} - {{doc}} {{/errors}} -func (class *Session) Async{{method_name_exported}}({{#func_params}}{{^first}}, {{/first}}{{name_internal}} {{type}}{{/func_params}}) (retval TaskRef, err error) { +func (class *Session) Async{{method_name_exported}}({{func_params}}) (retval TaskRef, err error) { method := "Async.{{class_name}}.{{method_name}}" {{#params}} - {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{#is_session_id}}class.ref{{/is_session_id}}{{^is_session_id}}{{name_internal}}{{/is_session_id}}) + {{name_internal}}Arg, err := serialize{{func_name_suffix}}(fmt.Sprintf("%s(%s)", method, "{{name}}"), {{name_in_serialize_call}}) if err != nil { return } @@ -63,4 +63,4 @@ func (class *Session) Async{{method_name_exported}}({{#func_params}}{{^first}}, } {{/async}} -{{/messages}} \ No newline at end of file +{{/messages}} From cca5a6e250b56a98294987a95b7ee53870188c41 Mon Sep 17 00:00:00 2001 From: Ming Lu Date: Wed, 12 Jun 2024 20:26:43 +0800 Subject: [PATCH 96/99] CP-49858: Unit test: Update for changes on template variables Signed-off-by: Ming Lu --- ocaml/sdk-gen/go/test_gen_go.ml | 65 ++++++++++++--------------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/ocaml/sdk-gen/go/test_gen_go.ml b/ocaml/sdk-gen/go/test_gen_go.ml index ff83521620f..8683099ff2c 100644 --- a/ocaml/sdk-gen/go/test_gen_go.ml +++ b/ocaml/sdk-gen/go/test_gen_go.ml @@ -98,21 +98,19 @@ let verify_error = function let param_keys = [ - "is_session_id" - ; "type" + "type" ; "name" ; "name_internal" + ; "name_in_serialize_call" ; "doc" ; "func_name_suffix" - ; "first" ] let verify_param_member = function - | "is_session_id", `Bool _ - | "first", `Bool _ | "type", `String _ | "name", `String _ | "name_internal", `String _ + | "name_in_serialize_call", `String _ | "doc", `String _ | "func_name_suffix", `String _ -> true @@ -140,6 +138,8 @@ let verify_message_member = function schema_check result_keys verify_result_member result | "params", `A params -> List.for_all verify_param params + | "func_params", `String _ -> + true | "errors", `A errors -> List.for_all verify_error errors | "async", `Bool _ | "has_error", `Bool _ | "errors", `Null -> @@ -147,7 +147,7 @@ let verify_message_member = function | _ -> false -let verify_sesseion_message_member = function +let verify_session_message_member = function | "method_name", `String _ | "class_name", `String _ | "class_name_exported", `String _ @@ -162,8 +162,8 @@ let verify_sesseion_message_member = function schema_check result_keys verify_result_member result | "params", `A params -> List.for_all verify_param params - | "func_params", `A params -> - List.for_all verify_param params + | "func_params", `String _ -> + true | "errors", `A errors -> List.for_all verify_error errors | "async", `Bool _ @@ -184,14 +184,14 @@ let message_keys = ; "description" ; "result" ; "params" + ; "func_params" ; "errors" ; "has_error" ; "async" ; "version" ] -let session_message_keys = - ["session_login"; "session_logout"; "func_params"] @ message_keys +let session_message_keys = ["session_login"; "session_logout"] @ message_keys let verify_message = function | `O members -> @@ -201,7 +201,7 @@ let verify_message = function if class_name <> `String "session" then schema_check message_keys verify_message_member members else - schema_check session_message_keys verify_sesseion_message_member members + schema_check session_message_keys verify_session_message_member members | _ -> false @@ -302,6 +302,8 @@ let verify_obj_member = function true | "modules", `O members -> schema_check modules_keys verify_modules_member members + | "licence", `String _ -> + true | _ -> false @@ -316,6 +318,7 @@ let obj_keys = ; "event" ; "session" ; "option" + ; "licence" ] let verify_obj = function @@ -834,28 +837,7 @@ let session_messages : Mustache.Json.t = ) ; ("async", `Bool false) ; ("version", `String "miami") - ; ( "func_params" - , `A - [ - `O - [ - ("type", `String "string") - ; ("name", `String "uname") - ; ("name_internal", `String "uname") - ; ("func_name_suffix", `String "String") - ; ("first", `Bool true) - ; ("is_session_id", `Bool false) - ] - ; `O - [ - ("type", `String "string") - ; ("name", `String "pwd") - ; ("name_internal", `String "pwd") - ; ("func_name_suffix", `String "String") - ; ("is_session_id", `Bool false) - ] - ] - ) + ; ("func_params", `String "uname string, pwd string") ; ( "params" , `A [ @@ -864,17 +846,16 @@ let session_messages : Mustache.Json.t = ("type", `String "string") ; ("name", `String "uname") ; ("name_internal", `String "uname") + ; ("name_in_serialize_call", `String "uname") ; ("func_name_suffix", `String "String") - ; ("first", `Bool true) - ; ("is_session_id", `Bool false) ] ; `O [ ("type", `String "string") ; ("name", `String "pwd") ; ("name_internal", `String "pwd") + ; ("name_in_serialize_call", `String "pwd") ; ("func_name_suffix", `String "String") - ; ("is_session_id", `Bool false) ] ] ) @@ -910,7 +891,7 @@ let session_messages : Mustache.Json.t = ; ("method_name_exported", `String "Logout") ; ("description", `String "Logout Log out of a session") ; ("async", `Bool false) - ; ("func_params", `A []) + ; ("func_params", `String "") ; ("version", `String "miami") ; ( "params" , `A @@ -920,8 +901,8 @@ let session_messages : Mustache.Json.t = ("type", `String "SessionRef") ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") + ; ("name_in_serialize_call", `String "class.ref") ; ("func_name_suffix", `String "SessionRef") - ; ("is_session_id", `Bool true) ] ] ) @@ -948,6 +929,7 @@ let messages : Mustache.Json.t = ; ("description", `String "GetLog Get the host log file") ; ("async", `Bool true) ; ("version", `String "miami") + ; ("func_params", `String "session *Session, host HostRef") ; ( "params" , `A [ @@ -956,18 +938,18 @@ let messages : Mustache.Json.t = ("type", `String "SessionRef") ; ("name", `String "session_id") ; ("name_internal", `String "sessionID") + ; ("name_in_serialize_call", `String "session.ref") ; ("func_name_suffix", `String "SessionRef") ; ("session", `Bool true) ; ("session_class", `Bool false) - ; ("first", `Bool true) ] ; `O [ ("type", `String "HostRef") ; ("name", `String "host") ; ("name_internal", `String "host") + ; ("name_in_serialize_call", `String "host") ; ("func_name_suffix", `String "HostRef") - ; ("first", `Bool false) ] ] ) @@ -1116,7 +1098,8 @@ module TemplatesTest = Generic.MakeStateless (struct end) module TestGeneratedJson = struct - let verify description verify_func actual = + let verify name verify_func actual = + let description = Printf.sprintf "Object name: %s" name in Alcotest.(check bool) description true (verify_func actual) let test_enums () = From 6319a73e470d334aca0b5887506b9b4551e0b299 Mon Sep 17 00:00:00 2001 From: Gabriel Buica Date: Fri, 14 Jun 2024 10:24:43 +0000 Subject: [PATCH 97/99] rpm: remove `sexprpp` from public_name Remove `package` and `public_name` stanzas for `sexprpp` executable. Signed-off-by: Gabriel Buica --- ocaml/libs/sexpr/dune | 2 -- 1 file changed, 2 deletions(-) diff --git a/ocaml/libs/sexpr/dune b/ocaml/libs/sexpr/dune index 02fa8cf9c7f..8f1c2a0e0ef 100644 --- a/ocaml/libs/sexpr/dune +++ b/ocaml/libs/sexpr/dune @@ -17,8 +17,6 @@ (executable (modes exe) (name sexprpp) - (public_name sexprpp) - (package sexpr) (modules sexprpp) (libraries sexpr From 703f8bbffec08c300c8c6ec7754ce997718f7884 Mon Sep 17 00:00:00 2001 From: Pau Ruiz Safont Date: Wed, 12 Jun 2024 16:40:40 +0100 Subject: [PATCH 98/99] sexpr: add tests to the package This fixes opam-based builds in xs-opam Signed-off-by: Pau Ruiz Safont --- ocaml/libs/sexpr/test/dune | 1 + sexpr.opam | 1 + 2 files changed, 2 insertions(+) diff --git a/ocaml/libs/sexpr/test/dune b/ocaml/libs/sexpr/test/dune index 2e60f3cd9b6..aa62e13e4e0 100644 --- a/ocaml/libs/sexpr/test/dune +++ b/ocaml/libs/sexpr/test/dune @@ -1,4 +1,5 @@ (test (name test_sexpr) + (package sexpr) (modules test_sexpr) (libraries sexpr astring rresult qcheck-core alcotest threads)) diff --git a/sexpr.opam b/sexpr.opam index 49226ada780..aded988a188 100644 --- a/sexpr.opam +++ b/sexpr.opam @@ -13,6 +13,7 @@ depends: [ "ocaml" "dune" "astring" + "qcheck-core" {with-test} "xapi-stdext-threads" ] synopsis: "Library required by xapi" From 597e50ccf058aa0590e35aec944539a9d4f6b61c Mon Sep 17 00:00:00 2001 From: Stephen Cheng Date: Mon, 17 Jun 2024 05:52:23 +0100 Subject: [PATCH 99/99] Fix pytype errors Signed-off-by: Stephen Cheng --- pyproject.toml | 1 - scripts/examples/python/XenAPI/XenAPI.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9b701e4ed6..2730c0ac018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -255,7 +255,6 @@ expected_to_fail = [ # SSLSocket.send() only accepts bytes, not unicode string as argument: "scripts/examples/python/exportimport.py", # Other fixes needed: - "scripts/examples/python/XenAPI/XenAPI.py", "scripts/examples/python/monitor-unwanted-domains.py", "scripts/examples/python/shell.py", "scripts/static-vdis", diff --git a/scripts/examples/python/XenAPI/XenAPI.py b/scripts/examples/python/XenAPI/XenAPI.py index 0211fe5e9c8..c4c71e4445e 100644 --- a/scripts/examples/python/XenAPI/XenAPI.py +++ b/scripts/examples/python/XenAPI/XenAPI.py @@ -54,6 +54,7 @@ # OF THIS SOFTWARE. # -------------------------------------------------------------------- +import errno import gettext import os import socket @@ -141,8 +142,8 @@ class Session(xmlrpclib.ServerProxy): session.xenapi.session.logout() """ - def __init__(self, uri, transport=None, encoding=None, verbose=0, - allow_none=1, ignore_ssl=False): + def __init__(self, uri, transport=None, encoding=None, verbose=False, + allow_none=True, ignore_ssl=False): # Fix for CA-172901 (+ Python 2.4 compatibility) # Fix for context=ctx ( < Python 2.7.9 compatibility) @@ -198,7 +199,7 @@ def _login(self, method, params): self.last_login_params = params self.API_version = self._get_api_version() except socket.error as e: - if e.errno == socket.errno.ETIMEDOUT: + if e.errno == errno.ETIMEDOUT: raise xmlrpclib.Fault(504, 'The connection timed out') else: raise e @@ -206,7 +207,7 @@ def _login(self, method, params): def _logout(self): try: if self.last_login_method.startswith("slave_local"): - return _parse_result(self.session.local_logout(self._session)) + return _parse_result(self.session.local_logout(self._session)) # pytype: disable=attribute-error else: return _parse_result(self.session.logout(self._session)) finally: