Skip to main content
Version: 7.16

User-Defined Metadata API

Introduction to the API

NetObserv Flow and NetObserv SNMP provide an API for managing User-Defined Metadata (UDM). The API documented here can be accessed via REST, gRPC, or connectrpc. REST calls can be made manually via curl, and gRPC calls can be made via grpcurl. If you want to access the built-in reflection capability, you must ust gRPC/grpcurl.

The API allows for creating, reading, updating, and deleting Udms, both individually and in bulk. (Except you can only delete individual Udms.)

caution

Using any of the create or update calls of the API (e.g. CreateUdm), collectively called "writes," will delete all comments in the YAML file, and probably change the order of stanzas in the file. The resulting file will be just the data in the file, not the comments or other formatting. So if you plan to make updates via the API, make sure there's no important information in the comments.

All RPCs are safe to call concurrently. The "read" RPCs will not block normal functioning of the collector, but the "write" RPCs will (very briefly), while the database is being updated and saved to disk. The larger the database, the longer the delay, of course, since it's just a YAML file and must be rewritten in its entirety for every write operation.

Write operations are atomic and include updating the YAML file. If a write operation fails for any reason (returns an error), then neither the internal database nor the external YAML file were updated.

info

Keys are given as strings and parsed as IP addresses, CIDRs (IP prefixes), or IP ranges (a "from" IP and a "to" IP). In the API, if any key cannot be parsed, the operation will abort and return an error listing the problematic key.

caution

Reminder: Updating the enrichment database via the API and manually editing the YAML file is prohibited. If you edit the YAML file and then update via the API, the API will overwrite your changes to the file.

API methods

CountUdms(google.protobuf.Empty) returns (CountUdmsResponse)

Return the number of items in the DB.

ListUdmKeys(ListUdmKeysRequest) returns (ListUdmKeysResponse)

Return keys matching the given key, using the same matching as ListUdms, above. (Basically, it calls ListUdms on the given key, and then returns only the keys of the returned Udms.) If no key is given, returns all keys.

ListUdms(ListUdmsRequest) returns (ListUdmsResponse)

Return the records matching the given key. Non-string metadata values (e.g., numbers) are returned as strings.

If no key is given, it returns the entire DB.

If exact is true, the key is matched exactly

If exact is false, all DB entries matching the requested key are returned, using the following rules:

  • key is a key as listed in the ipaddr.yml file

    If key is not specified, then exact is ignored and all Udms will be returned.

  • If the key is:

    • An IP address
      • That IP will be returned if it exists.
      • Any CIDR that contains the given IP will be returned.
      • Any Range that contains the given IP will be returned.
    • A CIDR
      • Any IP within the given CIDR will be returned.
      • Any CIDR within the given CIDR will be returned.
      • Any Range within the given CIDR will be returned. That is, if the first and last IP in the range is in the given CIDR.
    • A Range
      • Any IP within the given Range will be returned.
      • Any CIDR within the given Range will be returned. That is, the first and last IP in the CIDR are in the given Range. E.g., the first and last IPs in 192.168.1.0/24 are 192.168.1.0 and 192.168.1.255.
      • Any Range within the given Range will be returned. That is, for a given range R1, a range R2 in the database will be returned if R1.From <= R2.From AND R2.To <= R1.To.

As mentioned above, ListUdms is safe to call concurrently; if you have multiple keys to query, you can safely call ListUdms concurrently for each key.

CreateUdm(CreateUdmRequest) returns (Udm)

Create the given key/value and return it. Return an error if the given key already exists.

BatchCreateUdms(BatchCreateUdmsRequest) returns (BatchCreateUdmsResponse)

Create the given set of Udms and return them. Return an error if any of the given Udms already exist.

Creates are atomic and include saving the updates to the yml file.

DeleteUdm(DeleteUdmRequest) returns (google.protobuf.Empty)

Deletes the given Udm; the key must match exactly

Deletes are atomic and include updating the yml file.

UpdateUdm(UpdateUdmRequest) returns (Udm)

Update the given Udm with the given data. The UpdateMask field inside the request specifies which fields to update. If the UpdateMask is empty, all fields are updated, including those that were not specified in the update call (which will be set to their "zero value"). (See the example below for more details.)

Return the updated Udm or an error if the given key does not exist.

BatchUpdateUdms(BatchUpdateUdmsRequest) returns (BatchUpdateUdmsResponse)

Update the given Udms with the given data. The UpdateMask field inside the request specifies which fields to update. If UpdateMask is empty, all fields are updated, including those that were not specified in the update call (which will be set to their "zero value"). (See the example below for more details.)

Return the updated Udms or an error if any of the given keys do not exist.

Updates are atomic and include saving the updates to the yml file.

Implementation note: BatchUpdateUdms accepts a single BatchUpdatUdmsRequest struct containing a slice of UpdateUdmRequest structs, and a single UpdateMask field, which is used for the entire batch of updates. Each UpdateUdmRequest struct also has its own UpdateMask, which is ignored.

Individual types / messages

syntax = "proto3";

package udm.v1;

import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";

// Udm represents an IP enrichment entry.
message Udm {
string key = 1; // The key field will be populated from the yaml key when returning Udms.
string name = 2;
repeated string tags = 3;
uint64 vlan = 4;
bool internal = 5;
map<string, string> metadata = 6;
}

// CreateUdmRequest is the request for the CreateUdm RPC.
message CreateUdmRequest {
Udm udm = 1;
}

// BatchCreateUdmsRequest is the request for the BatchCreateUdms RPC.
message BatchCreateUdmsRequest {
repeated CreateUdmRequest requests = 1;
}

// BatchCreateUdmsResponse is the response for the BatchCreateUdms RPC.
message BatchCreateUdmsResponse {
// Udms created
repeated Udm udms = 1;
}

// UpdateUdmRequest is the request for the UpdateUdm RPC.
message UpdateUdmRequest {
Udm udm = 1;
optional google.protobuf.FieldMask update_mask = 2;
}

// BatchUpdateUdmsRequest is the request for the BatchUpdateUdms RPC.
message BatchUpdateUdmsRequest {
repeated UpdateUdmRequest requests = 1;
// update_mask fields in requests list are ignored
optional google.protobuf.FieldMask update_mask = 2;
}

message google.protobuf.FieldMask {
repeated string paths = 1;
}

// BatchUpdateUdmsResponse is the response for the BatchUpdateUdms RPC.
message BatchUpdateUdmsResponse {
repeated Udm udms = 1;
}

// ListUdmsRequest is the request for the ListUdms RPC.
message ListUdmsRequest {
string key = 1;
bool exact = 2;
}

// ListUdmsResponse is the response for the ListUdms RPC.
message ListUdmsResponse {
repeated Udm udms = 1;
}

// CountUdmsResponse is the response for the CountUdms RPC.
message CountUdmsResponse {
int32 count = 1;
}

// ListUdmKeysRequest is the request for the ListUdmKeys RPC.
message ListUdmKeysRequest {
string key = 1;
bool exact = 2;
}

// ListUdmKeysResponse is the response for the ListUdmKeys RPC.
message ListUdmKeysResponse {
repeated string keys = 1;
}

// DeleteUdmRequest is the request for the DeleteUdm RPC.
message DeleteUdmRequest {
string key = 1;
}

API examples and documentation using curl

Example configuration

flowcoll.yml:

EF_API_BASIC_AUTH_ENABLE: true
EF_API_BASIC_AUTH_PASSWORD: password
EF_API_BASIC_AUTH_USERNAME: username
EF_API_IP: 0.0.0.0
EF_API_PORT: 8080
EF_API_TLS_ENABLE: true
EF_API_TLS_CERT_FILEPATH: /etc/elastiflow/metadata/netobserv.crt
EF_API_TLS_KEY_FILEPATH: /etc/elastiflow/metadata/netobserv.key

EF_PROCESSOR_ENRICH_IPADDR_METADATA_ENABLE: true
EF_PROCESSOR_ENRICH_IPADDR_METADATA_USERDEF_PATH: /etc/elastiflow/metadata/ipaddr.yml
EF_PROCESSOR_ENRICH_IPADDR_METADATA_API_ENABLE: true

ipaddr.yml:

192.0.2.0/24:
internal: true
192.0.2.192/26:
name: atlanta_guest_wifi
vlan: 1001
tags:
- wifi
- dhcp
metadata:
dhcp.pool.name: atlanta_guest_wifi
.site.id: atlanta
192.0.2.194-192.0.2.198:
metadata:
.site.bldg.id: hq
.site.floor.id: 2
.site.rack.id: 1
192.0.2.194:
metadata:
device.type.name: wifi_ap

Given the following shell function:

# See below for an explanation of this function.
function api-curl {
curl --cacert /etc/elastiflow/metadata/netobserv.crt -u username:password -s \
--header "Content-Type: application/json" \
-d "$1" \
"https://localhost:8080/api/v1/enrich/ipaddr/udm.v1.UdmService/$2" \
| jq .
}

This function is to make the examples below easier to read. It takes a JSON payload and an API method name as arguments, and returns the JSON response, filtered through jq so it looks nice. It sets username and password to correspond to the configured values in flowcoll.yml (which in this example are just "username" and "password"). It also sets the TLS certificate to the one specified in flowcoll.yml.

If you do not configure basic authorization in flowcoll.yml, then you can omit the -u username:password argument; if you do not configure TLS in flowcoll.yml, then you can omit the TLS certificate argument, and change the URL to just http://... instead of https://....

CountUdms

api-curl '{}' CountUdms
{
"count": 4
}

ListUdmKeys

List all keys

api-curl '{}' ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

List a specific key

api-curl '{"key":"192.0.2.194", "exact":true}' ListUdmKeys
{
"keys": [
"192.0.2.194"
]
}

List all keys matching a given key

api-curl '{"key":"192.0.2.200", "exact":false}' ListUdmKeys
{
"keys": [
# Both of these CIDRs contain the given IP.
"192.0.2.0/24",
"192.0.2.192/26"
]
}

See the ListUdms description above for more details on the matching behavior.

ListUdms

api-curl '{"key":"192.0.2.200", "exact":false}' ListUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"internal": true
},
{
"key": "192.0.2.192/26",
"name": "atlanta_guest_wifi",
"tags": [
"wifi",
"dhcp"
],
"vlan": "1001",
"metadata": {
".site.id": "atlanta",
"dhcp.pool.name": "atlanta_guest_wifi"
}
}
]
}

See the ListUdms description above for more details on the matching behavior.

CreateUdm

CreateUdm returns the created Udm.

api-curl '{"udm": { "key": "192.0.3.0/24", "name": "my_udm", "vlan": 2, "tags":["tag1", "tag2"]}}' CreateUdm
{
"key": "192.0.3.0/24",
"name": "my_udm",
"tags": [
"tag1",
"tag2"
],
"vlan": "2"
}

BatchCreateUdms

BatchCreateUdms returns the created Udms.

api-curl '{"requests":[ 
{"udm": { "key": "192.0.4.0/24", "name": "my_udm2", "vlan": 4, "tags":["tag3", "tag4"]}},
{"udm": { "key": "192.0.5.0/24", "name": "my_udm3", "vlan": 5, "tags":["tag5", "tag6"]}}
]}' BatchCreateUdms
{
"udms": [
{
"key": "192.0.4.0/24",
"name": "my_udm2",
"tags": [
"tag3",
"tag4"
],
"vlan": "4"
},
{
"key": "192.0.5.0/24",
"name": "my_udm3",
"tags": [
"tag5",
"tag6"
],
"vlan": "5"
}
]
}

The current status of the DB after running the above example commands:

api-curl '{}' ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26",
"192.0.3.0/24",
"192.0.4.0/24",
"192.0.5.0/24",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

DeleteUdm

api-curl '{"key": "192.0.3.0/24"}' DeleteUdm
{}

api-curl '{}' ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26",
# note 192.0.3.0/24 is gone
"192.0.4.0/24",
"192.0.5.0/24",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

UpdateUdm

Without an update_mask

api-curl '{"key":"192.0.4.0/24"}' ListUdms
{
"udms": [
{
"key": "192.0.4.0/24",
"name": "my_udm2",
"tags": [
"tag3",
"tag4"
],
"vlan": "4"
}
]
}
api-curl '{"udm":{"key": "192.0.4.0/24", "vlan": 5}}' UpdateUdm
{
"key": "192.0.4.0/24",
"vlan": "5"
}

This command didn't give an update_mask, so all the fields not specified in the udm object were cleared.

With an update_mask

api-curl '{"key":"192.0.5.0/24"}' ListUdms
{
"udms": [
{
"key": "192.0.5.0/24",
"name": "my_udm3",
"tags": [
"tag5",
"tag6"
],
"vlan": "5"
}
]
}
# Only update vlan and tags fields
api-curl '{"udm":{"key": "192.0.5.0/24", "vlan": 6, "tags":["tag7"]}, "update_mask": "vlan,tags"}' UpdateUdm
{
"key": "192.0.5.0/24",
"name": "my_udm3", # name not changed
"tags": [ # all tags replaced
"tag7"
],
"vlan": "6" # vlan updated
}

This command did give an update_mask, so only the vlan and tags fields were updated.

important

update_mask in JSON input is parsed as a comma-separated list of field names.

Do not specify it as update_mask: {"paths":["vlan","tags"]}, even though the above UpdateUdmRequest message definition shows it that way.

BatchUpdateUdms

Without an update_mask

api-curl '{"requests":[ {"udm": { "key": "192.0.2.0/24", "name": "new_name"}},
{"udm": { "key": "192.0.2.194", "name": "new_name2"}} ]}' BatchUpdateUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"name": "new_name"
},
{
"key": "192.0.2.194",
"name": "new_name2"
}
]
}

As above, since no update_mask was specified, all fields not mentioned were cleared.

With an update_mask

  • Update only the tags and metadata fields.
  • Even though vlan is listed in the first UDM, it is not updated.
  • Even though name is listed in both UDMs, it is not updated.
  • Only the top-level update_mask field is valid. Even though the first request says "update_mask":"name", this is ignored.
# Update only the tags and metadata fields; inner "update_mask":"name" is ignored
api-curl '{"requests":[
{"udm": { "key": "192.0.2.0/24", "name": "not_changed", "vlan":2, "tags":["tag8"]}, "update_mask": "name"},
{"udm": { "key": "192.0.2.194", "name": "also_not_changed", "metadata":{"key":"value"}}}
],
"update_mask": "tags,metadata" }' BatchUpdateUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"name": "new_name",
"tags": [
"tag8"
]
},
{
"key": "192.0.2.194",
"name": "new_name2",
"metadata": {
"key": "value"
}
}
]
}
important

update_mask in JSON input is parsed as a comma-separated list of field names.

Do not specify it as update_mask: {"paths":["vlan","tags"]}, even though the above UpdateUdmRequest message definition shows it that way.

note

In a BatchUpdateUdms request, only the top-level update_mask field is used; the update_mask field in each request object is ignored.

API examples and documentation using grpcurl

Query the schema via gRPC reflection

gRPC is different from REST. It expects more structured input, and it provides reflection, so you can examine the provided services and messages at runtime.

As above, we'll be using a shell function to make the grpcurl examples easier to read. Like api-curl above, it specifies TLS and basic authentication.

function gcurl {
grpcurl -cacert /etc/elastiflow/metadata/netobserv.crt \
-H "Authorization: Basic $(echo -n username:password | base64)" \
"$@"
}
% gcurl localhost:8080 list
udm.v1.UdmService

% gcurl localhost:8080 describe
udm.v1.UdmService is a service:
service UdmService {
rpc BatchCreateUdms ( .udm.v1.BatchCreateUdmsRequest ) returns ( .udm.v1.BatchCreateUdmsResponse );
rpc BatchUpdateUdms ( .udm.v1.BatchUpdateUdmsRequest ) returns ( .udm.v1.BatchUpdateUdmsResponse );
rpc CountUdms ( .google.protobuf.Empty ) returns ( .udm.v1.CountUdmsResponse );
rpc CreateUdm ( .udm.v1.CreateUdmRequest ) returns ( .udm.v1.Udm );
rpc DeleteUdm ( .udm.v1.DeleteUdmRequest ) returns ( .google.protobuf.Empty );
rpc ListUdmKeys ( .udm.v1.ListUdmKeysRequest ) returns ( .udm.v1.ListUdmKeysResponse );
rpc ListUdms ( .udm.v1.ListUdmsRequest ) returns ( .udm.v1.ListUdmsResponse );
rpc UpdateUdm ( .udm.v1.UpdateUdmRequest ) returns ( .udm.v1.Udm );
}

% gcurl localhost:8080 describe .udm.v1.BatchCreateUdmsRequest
udm.v1.BatchCreateUdmsRequest is a message:
message BatchCreateUdmsRequest {
repeated .udm.v1.CreateUdmRequest requests = 1;
}

% gcurl localhost:8080 describe .udm.v1.CreateUdmRequest
udm.v1.CreateUdmRequest is a message:
message CreateUdmRequest {
.udm.v1.Udm udm = 1;
}

% gcurl localhost:8080 describe .udm.v1.Udm
udm.v1.Udm is a message:
message Udm {
string key = 1;
string name = 2;
repeated string tags = 3;
uint64 vlan = 4;
bool internal = 5;
map<string, string> metadata = 6;
}

See the grpcurl documentation for more details on using the gRPC reflection API.

note

I know of no way to get gRPC reflection to work with curl or in general via REST. You must use gRPC/grpcurl for that.

CountUdms

gcurl localhost:8080 udm.v1.UdmService/CountUdms
{
"count": 4
}

ListUdmKeys

List all keys

gcurl localhost:8080 udm.v1.UdmService/ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

List a specific key

gcurl -d '{"key":"192.0.2.194", "exact":true}' localhost:8080 udm.v1.UdmService/ListUdmKeys
{
"keys": [
"192.0.2.194"
]
}

List all keys matching a given key

gcurl -d '{"key":"192.0.2.200", "exact":false}' localhost:8080 udm.v1.UdmService/ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26"
]
}

ListUdms

gcurl -d '{"key":"192.0.2.200", "exact":false}' localhost:8080 udm.v1.UdmService/ListUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"internal": true
},
{
"key": "192.0.2.192/26",
"name": "atlanta_guest_wifi",
"tags": [
"wifi",
"dhcp"
],
"vlan": "1001",
"metadata": {
".site.id": "atlanta",
"dhcp.pool.name": "atlanta_guest_wifi"
}
}
]
}

See the ListUdms description above for more details on the matching behavior.

CreateUdm

CreateUdm returns the created Udm.

gcurl -d '{"udm": { "key": "192.0.3.0/24", "name": "my_udm", "vlan": 2, "tags":["tag1", "tag2"]}}' localhost:8080 udm.v1.UdmService/CreateUdm
{
"key": "192.0.3.0/24",
"name": "my_udm",
"tags": [
"tag1",
"tag2"
],
"vlan": "2"
}

BatchCreateUdms

BatchCreateUdms returns the created Udms.

gcurl -d '{"requests":[
{"udm": { "key": "192.0.4.0/24", "name": "my_udm2", "vlan": 4, "tags":["tag3", "tag4"]}},
{"udm": { "key": "192.0.5.0/24", "name": "my_udm3", "vlan": 5, "tags":["tag5", "tag6"]}}
]}' localhost:8080 udm.v1.UdmService/BatchCreateUdms
{
"udms": [
{
"key": "192.0.4.0/24",
"name": "my_udm2",
"tags": [
"tag3",
"tag4"
],
"vlan": "4"
},
{
"key": "192.0.5.0/24",
"name": "my_udm3",
"tags": [
"tag5",
"tag6"
],
"vlan": "5"
}
]
}

The current status of the DB after running the above example commands:

gcurl -d '{}' localhost:8080 udm.v1.UdmService/ListUdmKeys
{
"keys": [
"192.0.2.0/24",
"192.0.2.192/26",
"192.0.3.0/24",
"192.0.4.0/24",
"192.0.5.0/24",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

DeleteUdm

gcurl -d '{"key": "192.0.3.0/24"}' localhost:8080 udm.v1.UdmService/DeleteUdm
{}

gcurl -d '{}' localhost:8080 udm.v1.UdmService/ListUdmKeys
{
"keys": [
# Note 192.0.3.0/24 is gone
"192.0.4.0/24",
"192.0.5.0/24",
"192.0.2.0/24",
"192.0.2.192/26",
"192.0.2.194-192.0.2.198",
"192.0.2.194"
]
}

UpdateUdm

Without an update_mask

gcurl -d '{"key":"192.0.4.0/24"}' localhost:8080 udm.v1.UdmService/ListUdms
{
"udms": [
{
"key": "192.0.4.0/24",
"name": "my_udm2",
"tags": [
"tag3",
"tag4"
],
"vlan": "4"
}
]
}

gcurl -d '{"udm":{"key": "192.0.4.0/24", "vlan": 5}}' localhost:8080 udm.v1.UdmService/UpdateUdm
{
"key": "192.0.4.0/24",
"vlan": "5"
}

This command didn't give an update_mask, so all the fields not specified in the udm object were cleared.

With an update_mask

In gRPC, the update_mask field is a list of field names, not a comma-separated string.

gcurl -d '{"key":"192.0.5.0/24"}' localhost:8080 udm.v1.UdmService/ListUdms
{
"udms": [
{
"key": "192.0.5.0/24",
"name": "my_udm3",
"tags": [
"tag5",
"tag6"
],
"vlan": "5"
}
]
}

# Note the different format of the update_mask field.
gcurl -d '{"udm":{"key": "192.0.5.0/24", "vlan": 6, "tags":["tag7"]}, "update_mask": {"paths":["vlan","tags"]}}' localhost:8080 udm.v1.UdmService/UpdateUdm
{
"key": "192.0.5.0/24",
"name": "my_udm3", # name not changed
"tags": [ # all tags replaced
"tag7"
],
"vlan": "6" # vlan updated
}

This command did give an update_mask, so only the vlan and tags fields were updated.

important

update_mask in grpcurl must be a list of field names, as in the example above. (e.g. "update_mask": {"paths":["vlan","tags"]})

Do not specify it as update_mask: "vlan,tags". That's for REST calls.

If you are making native gRPC calls in Go or Java or similar, use the format appropriate for your language. Go might be something like this:

UpdateMask: &fieldmaskpb.FieldMask{ Paths: []string{"vlan", "tags"} }

BatchUpdateUdms

Without an update_mask

% gcurl -d '{"requests":[ {"udm": { "key": "192.0.2.0/24", "name": "new_name"}},
{"udm": { "key": "192.0.2.194", "name": "new_name2"}} ]}' localhost:8080 udm.v1.UdmService/BatchUpdateUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"name": "new_name"
},
{
"key": "192.0.2.194",
"name": "new_name2"
}
]
}

As above, since no update_mask was specified, all fields not mentioned were cleared.

With an update_mask

  • Note the different format of the update_mask, compared to the above curl example.
  • Update only the tags and metadata fields.
  • Even though vlan is listed in the first UDM, it is not updated.
  • Even though name is listed in both UDMs, it is not updated.
  • Only the top-level update_mask field is valid. Even though the first request says "update_mask":"name", this is ignored.
% gcurl -d '{"requests":[
{"udm": { "key": "192.0.2.0/24", "name": "not_changed", "vlan":2, "tags":["tag8"]}, "update_mask": {"paths":["name"]}},
{"udm": { "key": "192.0.2.194", "name": "also_not_changed", "metadata":{"key":"value"}}}
],
"update_mask": {"paths": ["tags","metadata"]} }' localhost:8080 udm.v1.UdmService/BatchUpdateUdms
{
"udms": [
{
"key": "192.0.2.0/24",
"name": "new_name",
"tags": [
"tag8"
]
},
{
"key": "192.0.2.194",
"name": "new_name2",
"metadata": {
"key": "value"
}
}
]
}
important

update_mask in grpcurl must be a list of field names, as in the example above. (e.g. "update_mask": {"paths":["vlan","tags"]})

Do not specify it as update_mask: "vlan,tags", that's for REST calls.

note

In a BatchUpdateUdms request, only the top-level update_mask field is used; the update_mask field in each request object is ignored.