diff --git a/cmd/ovsdb-check/main.go b/cmd/ovsdb-check/main.go new file mode 100644 index 00000000000..c3e4f5511ed --- /dev/null +++ b/cmd/ovsdb-check/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "yunion.io/x/ovsdb/client" + "yunion.io/x/ovsdb/schema/ovn_nb" +) + +func main() { + // 1. Connect to OVSDB (Adjust address as needed) + // Default OVN NB socket location + target := "unix:/var/run/ovn/ovnnb_db.sock" + + fmt.Printf("Connecting to %s...\n", target) + cli, err := client.NewClient(target) + if err != nil { + log.Fatalf("Failed to connect: %v\n(Make sure OVN is running and the socket path is correct)", err) + } + defer cli.Close() + log.Println("Connected to OVSDB") + + // 2. Setup Monitoring + // We use the generated OVNNorthbound struct as the target cache + dbCache := &ovn_nb.OVNNorthbound{} + ctx := context.Background() + + // Start monitoring in a goroutine (it blocks waiting for updates) + go func() { + log.Println("Starting Monitor...") + if err := cli.MonitorDB(ctx, "OVN_Northbound", dbCache); err != nil { + log.Printf("Monitor failed: %v", err) + } + }() + + // Wait for initial sync + fmt.Println("Waiting for cache sync...") + time.Sleep(1 * time.Second) + fmt.Printf("Cache Synced: %d Logical Switches found\n", len(dbCache.LogicalSwitch)) + + // 3. Perform a Transaction (Insert) + lsName := fmt.Sprintf("test-ls-%d", time.Now().Unix()) + newLs := &ovn_nb.LogicalSwitch{ + Name: lsName, + ExternalIds: map[string]string{ + "created-by": "go-client", + }, + } + + log.Printf("Creating Logical Switch: %s", lsName) + op := client.OvsdbCreateOp(newLs, "newSwitch") + + // Execute Transaction + results, err := cli.TransactOps(ctx, "OVN_Northbound", op) + if err != nil { + log.Fatalf("Transaction failed: %v", err) + } + + // Check Result + if len(results) > 0 && results[0].Error == "" { + fmt.Printf("Transaction Success! UUID: %v\n", results[0].Uuid) + } else { + fmt.Printf("Transaction Response: %+v\n", results) + } + + // 4. Verify Update in Cache + time.Sleep(200 * time.Millisecond) // Allow monitor update to arrive + found := false + for _, ls := range dbCache.LogicalSwitch { + if ls.Name == lsName { + fmt.Printf("Verified: Switch %s exists in local cache (UUID: %s)\n", lsName, ls.Uuid) + found = true + + // Cleanup: Delete the test switch + log.Printf("Cleaning up (Deleting %s)...", lsName) + delOp := client.OvsdbDeleteOp("Logical_Switch", client.NewConditionUuid(ls.Uuid)) + _, err := cli.TransactOps(ctx, "OVN_Northbound", delOp) + if err != nil { + log.Printf("Cleanup failed: %v", err) + } else { + log.Println("Cleanup successful.") + } + break + } + } + + if !found { + log.Printf("Error: Created switch not found in cache yet.") + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2e6aab043ae..add1528c083 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2353,6 +2353,7 @@ yunion.io/x/log/hooks # yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 ## explicit; go 1.14 yunion.io/x/ovsdb/cli_util +yunion.io/x/ovsdb/client yunion.io/x/ovsdb/schema/ovn_nb yunion.io/x/ovsdb/types # yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712 diff --git a/vendor/yunion.io/x/ovsdb/client/client.go b/vendor/yunion.io/x/ovsdb/client/client.go new file mode 100644 index 00000000000..ac02199a413 --- /dev/null +++ b/vendor/yunion.io/x/ovsdb/client/client.go @@ -0,0 +1,193 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strings" + "sync" + "sync/atomic" + + "yunion.io/x/log" +) + +type Client struct { + conn net.Conn + enc *json.Encoder + dec *json.Decoder + + seq uint64 + + pending map[uint64]chan *jsonRpcResponse + pendingMutex sync.Mutex + + monitorHandlers map[string]MonitorHandler + monitorHandlersMutex sync.Mutex + + stopCh chan struct{} + done chan struct{} +} + +type MonitorHandler func(tableUpdates *json.RawMessage) + +func NewClient(target string) (*Client, error) { + parts := strings.SplitN(target, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid target %s", target) + } + proto := parts[0] + addr := parts[1] + + conn, err := net.Dial(proto, addr) + if err != nil { + return nil, err + } + + c := &Client{ + conn: conn, + enc: json.NewEncoder(conn), + dec: json.NewDecoder(conn), + pending: make(map[uint64]chan *jsonRpcResponse), + monitorHandlers: make(map[string]MonitorHandler), + stopCh: make(chan struct{}), + done: make(chan struct{}), + } + + go c.readLoop() + + return c, nil +} + +func (c *Client) Close() error { + close(c.stopCh) + c.conn.Close() + <-c.done + return nil +} + +func (c *Client) readLoop() { + defer close(c.done) + for { + select { + case <-c.stopCh: + return + default: + } + + var rawMsg json.RawMessage + if err := c.dec.Decode(&rawMsg); err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + return + } + log.Errorf("decode error: %v", err) + // try to recover or exit? for now exit + return + } + + // Determine if it is a request, response or notification + // OVSDB server sends responses and notifications + + var resp jsonRpcResponse + if err := json.Unmarshal(rawMsg, &resp); err == nil && resp.Id != nil { + // It's a response + idVal, ok := resp.Id.(float64) // json numbers are floats + if !ok { + // check if it's string or other type? + // we use uint64 for id, so we expect number + // let's handle generic id + } + + c.pendingMutex.Lock() + var ch chan *jsonRpcResponse + // Find the channel + // Simple match for now, need robust ID handling + // We'll assume ID is integer + id := uint64(idVal) + if ch, ok = c.pending[id]; ok { + delete(c.pending, id) + } + c.pendingMutex.Unlock() + + if ch != nil { + ch <- &resp + } + continue + } + + var notif jsonRpcNotification + if err := json.Unmarshal(rawMsg, ¬if); err == nil && notif.Method == "update" { + // handle update + var params []json.RawMessage + if err := json.Unmarshal(*notif.Params, ¶ms); err != nil { + log.Errorf("failed to unmarshal update params: %v", err) + continue + } + if len(params) >= 2 { + var monId string + if err := json.Unmarshal(params[0], &monId); err != nil { + // maybe it is not string? RFC says json-value + // We will use string as monitor ID + } + + c.monitorHandlersMutex.Lock() + handler, ok := c.monitorHandlers[monId] + c.monitorHandlersMutex.Unlock() + + if ok { + handler(¶ms[1]) + } + } + continue + } + } +} + +func (c *Client) Call(ctx context.Context, method string, params []interface{}) (*json.RawMessage, error) { + id := atomic.AddUint64(&c.seq, 1) + req := jsonRpcRequest{ + Method: method, + Params: params, + Id: id, + } + + ch := make(chan *jsonRpcResponse, 1) + c.pendingMutex.Lock() + c.pending[id] = ch + c.pendingMutex.Unlock() + + defer func() { + c.pendingMutex.Lock() + delete(c.pending, id) + c.pendingMutex.Unlock() + }() + + if err := c.enc.Encode(req); err != nil { + return nil, err + } + + select { + case resp := <-ch: + if resp.Error != nil { + return nil, fmt.Errorf("rpc error: %v", resp.Error) + } + return resp.Result, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (c *Client) Monitor(ctx context.Context, dbName string, monitorId string, monitorRequests interface{}, handler MonitorHandler) (*json.RawMessage, error) { + c.monitorHandlersMutex.Lock() + c.monitorHandlers[monitorId] = handler + c.monitorHandlersMutex.Unlock() + + return c.Call(ctx, "monitor", []interface{}{dbName, monitorId, monitorRequests}) +} + +func (c *Client) Transact(ctx context.Context, dbName string, operations []interface{}) (*json.RawMessage, error) { + params := make([]interface{}, 0, 1+len(operations)) + params = append(params, dbName) + params = append(params, operations...) + return c.Call(ctx, "transact", params) +} diff --git a/vendor/yunion.io/x/ovsdb/client/monitor.go b/vendor/yunion.io/x/ovsdb/client/monitor.go new file mode 100644 index 00000000000..5d1f36a934f --- /dev/null +++ b/vendor/yunion.io/x/ovsdb/client/monitor.go @@ -0,0 +1,260 @@ +package client + +import ( + "context" + "encoding/json" + "reflect" + + "yunion.io/x/log" + "yunion.io/x/ovsdb/types" +) + +// MonitorDB sets up monitoring for all tables in the provided database model. +// The model must be a pointer to a struct where each field implements types.ITable. +// It updates the model in-place as notifications arrive. +func (c *Client) MonitorDB(ctx context.Context, dbName string, model types.IDatabase) error { + // 1. Inspect the model to find tables + val := reflect.ValueOf(model).Elem() + + monitorRequests := make(map[string]interface{}) + + tableMap := make(map[string]reflect.Value) + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + + if !field.CanInterface() { + continue + } + + if itbl, ok := field.Interface().(types.ITable); ok { + tblName := itbl.OvsdbTableName() + monitorRequests[tblName] = map[string]interface{}{ + "columns": nil, // monitor all columns + } + tableMap[tblName] = field + } else { + // Maybe it is a pointer to table? + // The generated code uses values for tables in the DB struct. + // e.g. LogicalSwitch LogicalSwitchTable + } + } + + // 2. Send monitor request + monitorId := "monid" // TODO: make unique + + // Handler for updates + handler := func(tableUpdates *json.RawMessage) { + if tableUpdates == nil { + return + } + var updates map[string]map[string]RowUpdate + if err := json.Unmarshal(*tableUpdates, &updates); err != nil { + log.Errorf("failed to unmarshal table updates: %v", err) + return + } + + c.applyUpdates(tableMap, updates) + } + + result, err := c.Monitor(ctx, dbName, monitorId, monitorRequests, handler) + if err != nil { + return err + } + + // 3. Apply initial result + handler(result) + + return nil +} + +type RowUpdate struct { + Old *json.RawMessage `json:"old,omitempty"` + New *json.RawMessage `json:"new,omitempty"` +} + +func (c *Client) applyUpdates(tableMap map[string]reflect.Value, updates map[string]map[string]RowUpdate) { + for tblName, rowUpdates := range updates { + field, ok := tableMap[tblName] + if !ok { + continue + } + + // field is the Table slice (e.g. LogicalSwitchTable) + // We need to update it. + // Since it's a slice, and we need to update it in place, we need to be careful. + // The field is a reflect.Value of the slice field in the struct. + // We can Set it. + + for uuid, rowUpdate := range rowUpdates { + if rowUpdate.New != nil { + // Insert or Modify + // Parse the new row + + // We need to create a new row instance of the correct type + // The table is a slice of RowType. + // We can get RowType from the slice type. + sliceType := field.Type() // LogicalSwitchTable (slice) + elemType := sliceType.Elem() // LogicalSwitch (struct) + + newRowPtr := reflect.New(elemType) // *LogicalSwitch + newRow := newRowPtr.Interface().(types.IRow) + + // Unmarshal into the new row + // But wait, the JSON format in OVSDB is ["uuid", "version", {columns}] or similar? + // RFC 7047 says: + // "new": + // is a JSON object with column names and values. + // Plus "_uuid" and "_version" are usually not in the columns map but are implicit or separate? + // Actually includes "_uuid" and "_version" if requested? + // The monitor request "columns": nil means all columns. + // RFC 7047 4.1.5: "The "new" member ... contain the contents of the row... + // The columns member of the row object contains the values of the columns..." + // Wait, is just the object { "col1": val1, ... } + + // Our generated structs expect to be unmarshaled from something? + // cli_util.UnmarshalJSON handles the format from ovn-nbctl. + // Here we have raw JSON from JSON-RPC. + // We need to manually map the JSON fields to the struct fields using SetColumn? + + // Let's parse the JSON into map[string]interface{} + var rowMap map[string]interface{} + if err := json.Unmarshal(*rowUpdate.New, &rowMap); err != nil { + log.Errorf("failed to unmarshal row: %v", err) + continue + } + + // Set _uuid if not present (it should be the key of the map) + // Actually the UUID is the key in tableUpdates map. + // The object might contain _uuid and _version if we are lucky/configured. + // But we definitely have the UUID from the map key. + + // We need to set UUID on the row object. + // The generated struct has Uuid field, but it's not exposed via SetColumn usually. + // Wait, generated code: + // func (row *LogicalSwitch) OvsdbUuid() string { return row.Uuid } + // But no SetUuid. + // However, since we have a pointer to the struct, we can set the field using reflection if we know the field name "Uuid". + + // Let's try to populate the row. + err := c.populateRow(newRow, uuid, rowMap) + if err != nil { + log.Errorf("failed to populate row: %v", err) + continue + } + + if rowUpdate.Old == nil { + // Insert + // Append to slice + // But we should check if it already exists? + // Monitor should give us consistent state. + // If it's initial result, it's insert. + + // AppendRow works on types.ITable but that's for value receiver usually in generated code? + // func (tbl *LogicalSwitchTable) AppendRow(irow types.IRow) + // Yes, it takes pointer receiver. + + // But field is the value of the slice field. + // We need to address it. + // field.Addr() can be used if the field is addressable. + // val (the struct) is addressable? + // val = reflect.ValueOf(model).Elem(). + // If model is pointer, val is addressable. + + // We can call AppendRow via interface if we cast field.Addr().Interface() to ITable + // But field.Type() is LogicalSwitchTable. + // *LogicalSwitchTable implements ITable. + + // So: + ptrToTable := field.Addr() + if itbl, ok := ptrToTable.Interface().(types.ITable); ok { + itbl.AppendRow(newRow) + } else { + log.Errorf("table %s does not implement ITable", tblName) + } + + } else { + // Modify + // We need to find the row with the UUID and update it. + // Scan the slice. + found := false + sliceLen := field.Len() + for i := 0; i < sliceLen; i++ { + rowVal := field.Index(i) + // rowVal is LogicalSwitch (struct) + // We need pointer to it to call OvsdbUuid + if rowVal.Addr().Interface().(types.IRow).OvsdbUuid() == uuid { + // Found it. Replace it or update it. + field.Index(i).Set(newRowPtr.Elem()) + found = true + break + } + } + if !found { + // Treat as insert? + ptrToTable := field.Addr() + if itbl, ok := ptrToTable.Interface().(types.ITable); ok { + itbl.AppendRow(newRow) + } + } + } + + } else { + // Delete (New is nil, Old is not nil) + // Remove from slice + sliceLen := field.Len() + newSlice := reflect.MakeSlice(field.Type(), 0, sliceLen) + + for i := 0; i < sliceLen; i++ { + rowVal := field.Index(i) + if rowVal.Addr().Interface().(types.IRow).OvsdbUuid() == uuid { + // Skip this one + continue + } + newSlice = reflect.Append(newSlice, rowVal) + } + field.Set(newSlice) + } + } + } +} + +func (c *Client) populateRow(row types.IRow, uuid string, data map[string]interface{}) error { + // Set UUID + // Use reflection to set "Uuid" field + val := reflect.ValueOf(row).Elem() + uuidField := val.FieldByName("Uuid") + if uuidField.IsValid() && uuidField.CanSet() { + uuidField.SetString(uuid) + } + + // Set _version if present + if v, ok := data["_version"]; ok { + if vs, ok := v.(string); ok { + verField := val.FieldByName("Version") + if verField.IsValid() && verField.CanSet() { + verField.SetString(vs) + } + } + } + + // Set other columns + for k, v := range data { + if k == "_uuid" || k == "_version" { + continue + } + // v could be atomic or ["uuid", "..."] or ["set", [...]] or ["map", [...]] + // The generated code's SetColumn expects the value to be compatible or handles conversion? + // Let's check schema_gen.go: Ensure... functions. + + // types.Ensure... functions need to be checked. + // They likely handle OVSDB JSON format. + + if err := row.SetColumn(k, v); err != nil { + // log.Warningf("failed to set column %s: %v", k, err) + // return err + // Don't fail completely on one column error? + } + } + return nil +} diff --git a/vendor/yunion.io/x/ovsdb/client/rpc.go b/vendor/yunion.io/x/ovsdb/client/rpc.go new file mode 100644 index 00000000000..b4f9d5082ff --- /dev/null +++ b/vendor/yunion.io/x/ovsdb/client/rpc.go @@ -0,0 +1,52 @@ +package client + +import ( + "encoding/json" +) + +type jsonRpcRequest struct { + Method string `json:"method"` + Params []interface{} `json:"params"` + Id interface{} `json:"id"` +} + +type jsonRpcResponse struct { + Result *json.RawMessage `json:"result"` + Error interface{} `json:"error"` + Id interface{} `json:"id"` +} + +type jsonRpcNotification struct { + Method string `json:"method"` + Params *json.RawMessage `json:"params"` + Id interface{} `json:"id"` // should be null +} + +type Operation struct { + Op string `json:"op"` + Table string `json:"table,omitempty"` + // Common fields + Where []interface{} `json:"where,omitempty"` + Columns []string `json:"columns,omitempty"` + // Insert/Update + Row map[string]interface{} `json:"row,omitempty"` + UuidName string `json:"uuid-name,omitempty"` + // Mutate + Mutations []interface{} `json:"mutations,omitempty"` + // Wait + Timeout *int64 `json:"timeout,omitempty"` + Until string `json:"until,omitempty"` + Rows []interface{} `json:"rows,omitempty"` + // Comment + Comment string `json:"comment,omitempty"` +} + +// Helpers for conditions +func NewCondition(column string, function string, value interface{}) []interface{} { + return []interface{}{column, function, value} +} + +// Helpers for mutations +func NewMutation(column string, mutator string, value interface{}) []interface{} { + return []interface{}{column, mutator, value} +} diff --git a/vendor/yunion.io/x/ovsdb/client/schema.go b/vendor/yunion.io/x/ovsdb/client/schema.go new file mode 100644 index 00000000000..53229caa5e2 --- /dev/null +++ b/vendor/yunion.io/x/ovsdb/client/schema.go @@ -0,0 +1,186 @@ +package client + +// Updated Schema map with more precise types for serialization +var OvnNbSchemaTypes = map[string]map[string]string{ + "NB_Global": { + "nb_cfg": "integer", + "sb_cfg": "integer", + "hv_cfg": "integer", + "external_ids": "mapStringString", + "connections": "setUuid", + "ssl": "setUuid", + "options": "mapStringString", + "ipsec": "boolean", + }, + "Logical_Switch": { + "name": "string", + "ports": "setUuid", + "acls": "setUuid", + "qos_rules": "setUuid", + "load_balancer": "setUuid", + "dns_records": "setUuid", + "other_config": "mapStringString", + "external_ids": "mapStringString", + }, + "Logical_Switch_Port": { + "name": "string", + "type": "string", + "options": "mapStringString", + "parent_name": "string", + "tag_request": "integer", + "tag": "integer", + "addresses": "setString", + "dynamic_addresses": "string", + "port_security": "setString", + "up": "boolean", + "enabled": "boolean", + "dhcpv4_options": "uuid", + "dhcpv6_options": "uuid", + "ha_chassis_group": "uuid", + "external_ids": "mapStringString", + }, + "Address_Set": { + "name": "string", + "addresses": "setString", + "external_ids": "mapStringString", + }, + "Port_Group": { + "name": "string", + "ports": "setUuid", + "acls": "setUuid", + "external_ids": "mapStringString", + }, + "Load_Balancer": { + "name": "string", + "vips": "mapStringString", + "protocol": "string", + "external_ids": "mapStringString", + }, + "ACL": { + "name": "string", + "priority": "integer", + "direction": "string", + "match": "string", + "action": "string", + "log": "boolean", + "severity": "string", + "meter": "string", + "external_ids": "mapStringString", + }, + "QoS": { + "priority": "integer", + "direction": "string", + "match": "string", + "action": "mapStringInteger", + "bandwidth": "mapStringInteger", + "external_ids": "mapStringString", + }, + "Meter": { + "name": "string", + "unit": "string", + "bands": "setUuid", + "external_ids": "mapStringString", + }, + "Meter_Band": { + "action": "string", + "rate": "integer", + "burst_size": "integer", + "external_ids": "mapStringString", + }, + "Logical_Router": { + "name": "string", + "ports": "setUuid", + "static_routes": "setUuid", + "policies": "setUuid", + "enabled": "boolean", + "nat": "setUuid", + "load_balancer": "setUuid", + "options": "mapStringString", + "external_ids": "mapStringString", + }, + "Logical_Router_Port": { + "name": "string", + "gateway_chassis": "setUuid", + "ha_chassis_group": "uuid", + "options": "mapStringString", + "networks": "setString", + "mac": "string", + "peer": "string", + "enabled": "boolean", + "ipv6_ra_configs": "mapStringString", + "external_ids": "mapStringString", + }, + "Logical_Router_Static_Route": { + "ip_prefix": "string", + "policy": "string", + "nexthop": "string", + "output_port": "string", + "external_ids": "mapStringString", + }, + "Logical_Router_Policy": { + "priority": "integer", + "match": "string", + "action": "string", + "nexthop": "string", + "external_ids": "mapStringString", + }, + "NAT": { + "external_ip": "string", + "external_mac": "string", + "logical_ip": "string", + "logical_port": "string", + "type": "string", + "external_ids": "mapStringString", + }, + "DHCP_Options": { + "cidr": "string", + "options": "mapStringString", + "external_ids": "mapStringString", + }, + "Connection": { + "target": "string", + "max_backoff": "integer", + "inactivity_probe": "integer", + "other_config": "mapStringString", + "external_ids": "mapStringString", + }, + "DNS": { + "records": "mapStringString", + "external_ids": "mapStringString", + }, + "SSL": { + "private_key": "string", + "certificate": "string", + "ca_cert": "string", + "bootstrap_ca_cert": "boolean", + "ssl_protocols": "string", + "ssl_ciphers": "string", + "external_ids": "mapStringString", + }, + "Gateway_Chassis": { + "name": "string", + "chassis_name": "string", + "priority": "integer", + "external_ids": "mapStringString", + "options": "mapStringString", + }, + "HA_Chassis": { + "chassis_name": "string", + "priority": "integer", + "external_ids": "mapStringString", + }, + "HA_Chassis_Group": { + "name": "string", + "ha_chassis": "setUuid", + "external_ids": "mapStringString", + }, +} + +func GetSchemaType(table, column string) string { + if cols, ok := OvnNbSchemaTypes[table]; ok { + if t, ok := cols[column]; ok { + return t + } + } + return "string" // default +} diff --git a/vendor/yunion.io/x/ovsdb/client/transact.go b/vendor/yunion.io/x/ovsdb/client/transact.go new file mode 100644 index 00000000000..fd1c44ca80b --- /dev/null +++ b/vendor/yunion.io/x/ovsdb/client/transact.go @@ -0,0 +1,231 @@ +package client + +import ( + "context" + "encoding/json" + "reflect" + + "yunion.io/x/ovsdb/types" +) + +func (c *Client) TransactOps(ctx context.Context, dbName string, ops ...Operation) ([]OperationResult, error) { + opInterfaces := make([]interface{}, len(ops)) + for i, op := range ops { + opInterfaces[i] = op + } + + rawResult, err := c.Transact(ctx, dbName, opInterfaces) + if err != nil { + return nil, err + } + + // Parse result + var results []OperationResult + if err := json.Unmarshal(*rawResult, &results); err != nil { + return nil, err + } + + return results, nil +} + +type OperationResult struct { + Count int `json:"count,omitempty"` + Uuid []interface{} `json:"uuid,omitempty"` // ["uuid", "new-uuid"] + Rows []interface{} `json:"rows,omitempty"` + Error string `json:"error,omitempty"` + Details string `json:"details,omitempty"` +} + +func NewConditionUuid(uuid string) []interface{} { + return []interface{}{"_uuid", "==", []string{"uuid", uuid}} +} + +// ToOvsdbJSONRow converts an IRow to a map suitable for OVSDB JSON-RPC +func ToOvsdbJSONRow(row types.IRow) map[string]interface{} { + ret := make(map[string]interface{}) + + val := reflect.ValueOf(row).Elem() + typ := val.Type() + + tableName := row.OvsdbTableName() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + tag := fieldType.Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + + // Handle "name,omitempty" + // tag is `json:"name"` + colName := tag + if idx := reflect.ValueOf(tag).String(); len(idx) > 0 { + // manually parse? + // standard lib does it + } + // Just take until comma + commaIdx := 0 + for j := 0; j < len(tag); j++ { + if tag[j] == ',' { + commaIdx = j + break + } + } + if commaIdx > 0 { + colName = tag[:commaIdx] + } + + if colName == "_uuid" || colName == "_version" { + continue + } + + // Check if field should be skipped + if shouldSkip(field) { + continue + } + + // Get schema type + schemaType := GetSchemaType(tableName, colName) + + jsonVal := formatValue(field, schemaType) + ret[colName] = jsonVal + } + + return ret +} + +func shouldSkip(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + return v.IsNil() + case reflect.Slice, reflect.Map: + return v.IsNil() || v.Len() == 0 + } + // Keep primitives (Int, String, Bool, etc.) even if zero value + return false +} + +func formatValue(v reflect.Value, schemaType string) interface{} { + switch schemaType { + case "uuid": + // Value should be string or *string + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + return []string{"uuid", v.String()} + case "setUuid": + // Value should be []string + // OVSDB set format: ["set", [["uuid", "u1"], ["uuid", "u2"]]] + // Wait, RFC says set is ["set", [e1, e2, ...]] + // e1 is ["uuid", "u1"] + + sl := v + if sl.Len() == 0 { + return []interface{}{"set", []interface{}{}} + } + elems := make([]interface{}, 0, sl.Len()) + for i := 0; i < sl.Len(); i++ { + uuidStr := sl.Index(i).String() + elems = append(elems, []string{"uuid", uuidStr}) + } + return []interface{}{"set", elems} + case "setString": + sl := v + if sl.Len() == 0 { + return []interface{}{"set", []interface{}{}} + } + // If length is 1, can we send just the string? + // RFC 7047: "For a column with min 0 or 1 and max 1, is the value... + // For a column with max > 1, is ["set", [e1, ...]]" + // We assumed setString corresponds to max=unlimited. + // Let's adhere to set format always for set types. + elems := make([]interface{}, 0, sl.Len()) + for i := 0; i < sl.Len(); i++ { + elems = append(elems, sl.Index(i).Interface()) + } + return []interface{}{"set", elems} + case "mapStringString", "mapStringInteger", "mapStringIntegerMap", "mapStringIntegerReal", "mapStringBoolean": + // Map format: ["map", [[k1, v1], [k2, v2]]] + m := v + if m.Len() == 0 { + return []interface{}{"map", []interface{}{}} + } + elems := make([]interface{}, 0, m.Len()) + iter := m.MapRange() + for iter.Next() { + k := iter.Key().Interface() + val := iter.Value().Interface() + elems = append(elems, []interface{}{k, val}) + } + return []interface{}{"map", elems} + default: + // atomic types + if v.Kind() == reflect.Ptr { + return v.Elem().Interface() + } + return v.Interface() + } +} + +func OvsdbCreateOp(row types.IRow, uuidName string) Operation { + return Operation{ + Op: "insert", + Table: row.OvsdbTableName(), + Row: ToOvsdbJSONRow(row), + UuidName: uuidName, + } +} + +// For delete, we usually delete by UUID or condition +func OvsdbDeleteOp(table string, conditions []interface{}) Operation { + return Operation{ + Op: "delete", + Table: table, + Where: conditions, + } +} + +// For update, we update by condition (usually UUID) +func OvsdbUpdateOp(table string, conditions []interface{}, row types.IRow) Operation { + return Operation{ + Op: "update", + Table: table, + Where: conditions, + Row: ToOvsdbJSONRow(row), + } +} + +// Mutate is useful for adding/removing from set/map +func OvsdbMutateOp(table string, conditions []interface{}, mutations []interface{}) Operation { + return Operation{ + Op: "mutate", + Table: table, + Where: conditions, + Mutations: mutations, + } +} + +// Wait +func OvsdbWaitOp(table string, conditions []interface{}, timeout int64) Operation { + return Operation{ + Op: "wait", + Table: table, + Timeout: &timeout, + Where: conditions, + Until: "==", + Rows: []interface{}{}, // empty rows means wait until empty? No. + // wait until table has rows matching where? + // RFC: "wait": "waits until is true" + // "rows" member of result? No. + // We need "columns", "rows", "until". + // Default until is "==". + // columns default all. + // rows default empty? + // "If 'rows' is omitted, it defaults to an empty array." + // "waits until the query ... would return the result specified by 'rows'". + // So if we want to wait until a row exists, we should specify it? + // Usually we wait until specific condition matches nothing (deleted) or something (created). + } +}