blob: 87dee55e2486d1e1735148689ca49cb4ea46d8d6 [file] [log] [blame]
Scott Baker6cf525a2019-05-09 12:25:08 -07001/*
2 * Portions copyright 2019-present Open Networking Foundation
3 * Original copyright 2019-present Ciena Corporation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17package commands
18
19import (
20 "context"
21 "errors"
22 "fmt"
23 "github.com/fullstorydev/grpcurl"
Scott Baker5201c0b2019-05-15 15:35:56 -070024 "github.com/golang/protobuf/proto"
25 "github.com/golang/protobuf/protoc-gen-go/descriptor"
26 "github.com/jhump/protoreflect/desc"
Scott Baker6cf525a2019-05-09 12:25:08 -070027 "github.com/jhump/protoreflect/dynamic"
28 "google.golang.org/grpc"
Scott Baker5201c0b2019-05-15 15:35:56 -070029 "io"
30 "strconv"
Scott Baker6cf525a2019-05-09 12:25:08 -070031 "strings"
32 "time"
33)
34
Scott Baker5201c0b2019-05-15 15:35:56 -070035// Flags for calling the *WithRetry methods
Scott Baker6cf525a2019-05-09 12:25:08 -070036const GM_QUIET = 1
37const GM_UNTIL_FOUND = 2
38const GM_UNTIL_ENACTED = 4
39const GM_UNTIL_STATUS = 8
40
Scott Baker5201c0b2019-05-15 15:35:56 -070041type QueryEventHandler struct {
42 RpcEventHandler
43 Elements map[string]string
44 Model *desc.MessageDescriptor
45 Kind string
46 EOF bool
47}
48
49// Separate the operator from the query value.
50// For example,
51// "==foo" --> "EQUAL", "foo"
52func DecodeOperator(query string) (string, string, bool, error) {
53 if strings.HasPrefix(query, "!=") {
Scott Baker5281d002019-05-16 10:45:26 -070054 return strings.TrimSpace(query[2:]), "EQUAL", true, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070055 } else if strings.HasPrefix(query, "==") {
56 return "", "", false, errors.New("Operator == is now allowed. Suggest using = instead.")
57 } else if strings.HasPrefix(query, "=") {
Scott Baker5281d002019-05-16 10:45:26 -070058 return strings.TrimSpace(query[1:]), "EQUAL", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070059 } else if strings.HasPrefix(query, ">=") {
Scott Baker5281d002019-05-16 10:45:26 -070060 return strings.TrimSpace(query[2:]), "GREATER_THAN_OR_EQUAL", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070061 } else if strings.HasPrefix(query, ">") {
Scott Baker5281d002019-05-16 10:45:26 -070062 return strings.TrimSpace(query[1:]), "GREATER_THAN", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070063 } else if strings.HasPrefix(query, "<=") {
Scott Baker5281d002019-05-16 10:45:26 -070064 return strings.TrimSpace(query[2:]), "LESS_THAN_OR_EQUAL", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070065 } else if strings.HasPrefix(query, "<") {
Scott Baker5281d002019-05-16 10:45:26 -070066 return strings.TrimSpace(query[1:]), "LESS_THAN", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070067 } else {
Scott Baker5281d002019-05-16 10:45:26 -070068 return strings.TrimSpace(query), "EQUAL", false, nil
Scott Baker5201c0b2019-05-15 15:35:56 -070069 }
70}
71
72// Generate the parameters for Query messages.
73func (h *QueryEventHandler) GetParams(msg proto.Message) error {
74 dmsg, err := dynamic.AsDynamicMessage(msg)
75 if err != nil {
76 return err
77 }
78
79 //fmt.Printf("MessageName: %s\n", dmsg.XXX_MessageName())
80
81 if h.EOF {
82 return io.EOF
83 }
84
85 // Get the MessageType for the `elements` field
86 md := dmsg.GetMessageDescriptor()
87 elements_fld := md.FindFieldByName("elements")
88 elements_mt := elements_fld.GetMessageType()
89
90 for field_name, element := range h.Elements {
91 value, operator, invert, err := DecodeOperator(element)
92 if err != nil {
93 return err
94 }
95
96 nm := dynamic.NewMessage(elements_mt)
97
Scott Baker5281d002019-05-16 10:45:26 -070098 field_descriptor := h.Model.FindFieldByName(field_name)
99 if field_descriptor == nil {
100 return fmt.Errorf("Field %s does not exist", field_name)
101 }
102
103 field_type := field_descriptor.GetType()
104 switch field_type {
105 case descriptor.FieldDescriptorProto_TYPE_INT32:
106 var i int64
107 i, err = strconv.ParseInt(value, 10, 32)
Scott Baker5201c0b2019-05-15 15:35:56 -0700108 nm.SetFieldByName("iValue", int32(i))
Scott Baker5281d002019-05-16 10:45:26 -0700109 case descriptor.FieldDescriptorProto_TYPE_UINT32:
110 var i int64
111 i, err = strconv.ParseInt(value, 10, 32)
Scott Baker5201c0b2019-05-15 15:35:56 -0700112 nm.SetFieldByName("iValue", uint32(i))
Scott Baker5281d002019-05-16 10:45:26 -0700113 case descriptor.FieldDescriptorProto_TYPE_FLOAT:
114 err = errors.New("Floating point filters are unsupported")
115 case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
116 err = errors.New("Floating point filters are unsupported")
117 default:
Scott Baker5201c0b2019-05-15 15:35:56 -0700118 nm.SetFieldByName("sValue", value)
Scott Baker5281d002019-05-16 10:45:26 -0700119 err = nil
120 }
121
122 if err != nil {
123 return err
Scott Baker5201c0b2019-05-15 15:35:56 -0700124 }
125
126 nm.SetFieldByName("name", field_name)
127 nm.SetFieldByName("invert", invert)
128 SetEnumValue(nm, "operator", operator)
129 dmsg.AddRepeatedFieldByName("elements", nm)
130 }
131
132 SetEnumValue(dmsg, "kind", h.Kind)
133
134 h.EOF = true
135
136 return nil
137}
138
139// Take a string list of queries and turns it into a map of queries
Scott Baker5281d002019-05-16 10:45:26 -0700140func QueryStringsToMap(query_args []string, allow_inequality bool) (map[string]string, error) {
Scott Baker5201c0b2019-05-15 15:35:56 -0700141 queries := make(map[string]string)
142 for _, query_str := range query_args {
Scott Baker5281d002019-05-16 10:45:26 -0700143 query_str := strings.TrimSpace(query_str)
Scott Baker5201c0b2019-05-15 15:35:56 -0700144 operator_pos := -1
145 for i, ch := range query_str {
Scott Baker5281d002019-05-16 10:45:26 -0700146 if allow_inequality {
147 if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
148 operator_pos = i
149 break
150 }
151 } else {
152 if ch == '=' {
153 operator_pos = i
154 break
155 }
Scott Baker5201c0b2019-05-15 15:35:56 -0700156 }
157 }
158 if operator_pos == -1 {
Scott Baker5281d002019-05-16 10:45:26 -0700159 return nil, fmt.Errorf("Illegal operator/value string %s", query_str)
Scott Baker5201c0b2019-05-15 15:35:56 -0700160 }
Scott Baker5281d002019-05-16 10:45:26 -0700161 queries[strings.TrimSpace(query_str[:operator_pos])] = query_str[operator_pos:]
Scott Baker5201c0b2019-05-15 15:35:56 -0700162 }
163 return queries, nil
164}
165
166// Take a string of comma-separated queries and turn it into a map of queries
Scott Baker5281d002019-05-16 10:45:26 -0700167func CommaSeparatedQueryToMap(query_str string, allow_inequality bool) (map[string]string, error) {
Scott Baker5201c0b2019-05-15 15:35:56 -0700168 if query_str == "" {
169 return nil, nil
170 }
171
172 query_strings := strings.Split(query_str, ",")
Scott Baker5281d002019-05-16 10:45:26 -0700173 return QueryStringsToMap(query_strings, allow_inequality)
174}
175
176// Convert a string into the appropriate gRPC type for a given field
177func TypeConvert(source grpcurl.DescriptorSource, modelName string, field_name string, v string) (interface{}, error) {
178 model_descriptor, err := source.FindSymbol("xos." + modelName)
179 if err != nil {
180 return nil, err
181 }
182 model_md, ok := model_descriptor.(*desc.MessageDescriptor)
183 if !ok {
184 return nil, fmt.Errorf("Failed to convert model %s to a messagedescriptor", modelName)
185 }
186 field_descriptor := model_md.FindFieldByName(field_name)
187 if field_descriptor == nil {
188 return nil, fmt.Errorf("Field %s does not exist in model %s", field_name, modelName)
189 }
190 field_type := field_descriptor.GetType()
191
192 var result interface{}
193
194 switch field_type {
195 case descriptor.FieldDescriptorProto_TYPE_INT32:
196 var i int64
197 i, err = strconv.ParseInt(v, 10, 32)
198 result = int32(i)
199 case descriptor.FieldDescriptorProto_TYPE_UINT32:
200 var i int64
201 i, err = strconv.ParseInt(v, 10, 32)
202 result = uint32(i)
203 case descriptor.FieldDescriptorProto_TYPE_FLOAT:
204 var f float64
205 f, err = strconv.ParseFloat(v, 32)
206 result = float32(f)
207 case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
208 var f float64
209 f, err = strconv.ParseFloat(v, 64)
210 result = f
211 default:
212 result = v
213 err = nil
214 }
215
216 return result, err
Scott Baker5201c0b2019-05-15 15:35:56 -0700217}
218
Scott Baker6cf525a2019-05-09 12:25:08 -0700219// Return a list of all available model names
220func GetModelNames(source grpcurl.DescriptorSource) (map[string]bool, error) {
221 models := make(map[string]bool)
222 methods, err := grpcurl.ListMethods(source, "xos.xos")
223
224 if err != nil {
225 return nil, err
226 }
227
228 for _, method := range methods {
229 if strings.HasPrefix(method, "xos.xos.Get") {
230 models[method[11:]] = true
231 }
232 }
233
234 return models, nil
235}
236
237// Check to see if a model name is valid
238func CheckModelName(source grpcurl.DescriptorSource, name string) error {
239 models, err := GetModelNames(source)
240 if err != nil {
241 return err
242 }
243 _, present := models[name]
244 if !present {
245 return errors.New("Model " + name + " does not exist. Use `cordctl models available` to get a list of available models")
246 }
247 return nil
248}
249
250// Create a model in XOS given a map of fields
251func CreateModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fields map[string]interface{}) error {
252 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
253 defer cancel()
254
255 headers := GenerateHeaders()
256
257 h := &RpcEventHandler{
258 Fields: map[string]map[string]interface{}{"xos." + modelName: fields},
259 }
260 err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Create"+modelName, headers, h, h.GetParams)
261 if err != nil {
262 return err
263 } else if h.Status != nil && h.Status.Err() != nil {
264 return h.Status.Err()
265 }
266
267 resp, err := dynamic.AsDynamicMessage(h.Response)
268 if err != nil {
269 return err
270 }
271
272 fields["id"] = resp.GetFieldByName("id").(int32)
273
274 if resp.HasFieldName("uuid") {
275 fields["uuid"] = resp.GetFieldByName("uuid").(string)
276 }
277
278 return nil
279}
280
Scott Baker5281d002019-05-16 10:45:26 -0700281// Update a model in XOS given a map of fields
282func UpdateModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fields map[string]interface{}) error {
283 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
284 defer cancel()
285
286 headers := GenerateHeaders()
287
288 h := &RpcEventHandler{
289 Fields: map[string]map[string]interface{}{"xos." + modelName: fields},
290 }
291 err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Update"+modelName, headers, h, h.GetParams)
292 if err != nil {
293 return err
294 } else if h.Status != nil && h.Status.Err() != nil {
295 return h.Status.Err()
296 }
297
298 resp, err := dynamic.AsDynamicMessage(h.Response)
299 if err != nil {
300 return err
301 }
302
303 // TODO: Do we need to do anything with the response?
304 _ = resp
305
306 return nil
307}
308
Scott Baker6cf525a2019-05-09 12:25:08 -0700309// Get a model from XOS given its ID
310func GetModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, error) {
311 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
312 defer cancel()
313
314 headers := GenerateHeaders()
315
316 h := &RpcEventHandler{
317 Fields: map[string]map[string]interface{}{"xos.ID": map[string]interface{}{"id": id}},
318 }
319 err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Get"+modelName, headers, h, h.GetParams)
320 if err != nil {
321 return nil, err
322 }
323
324 if h.Status != nil && h.Status.Err() != nil {
325 return nil, h.Status.Err()
326 }
327
328 d, err := dynamic.AsDynamicMessage(h.Response)
329 if err != nil {
330 return nil, err
331 }
332
333 return d, nil
334}
335
336// Get a model, but retry under a variety of circumstances
337func GetModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
338 quiet := (flags & GM_QUIET) != 0
339 until_found := (flags & GM_UNTIL_FOUND) != 0
340 until_enacted := (flags & GM_UNTIL_ENACTED) != 0
341 until_status := (flags & GM_UNTIL_STATUS) != 0
342
343 for {
344 var err error
345
346 if conn == nil {
347 conn, err = NewConnection()
348 if err != nil {
349 return nil, nil, err
350 }
351 }
352
353 model, err := GetModel(conn, descriptor, modelName, id)
354 if err != nil {
355 if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
356 strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
357 if !quiet {
358 fmt.Print(".")
359 }
360 time.Sleep(100 * time.Millisecond)
361 conn.Close()
362 conn = nil
363 continue
364 }
365
366 if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
367 if !quiet {
368 fmt.Print("x")
369 }
370 time.Sleep(100 * time.Millisecond)
371 continue
372 }
373 return nil, nil, err
374 }
375
376 if until_enacted && !IsEnacted(model) {
377 if !quiet {
378 fmt.Print("o")
379 }
380 time.Sleep(100 * time.Millisecond)
381 continue
382 }
383
384 if until_status && model.GetFieldByName("status") == nil {
385 if !quiet {
386 fmt.Print("O")
387 }
388 time.Sleep(100 * time.Millisecond)
389 continue
390 }
391
392 return conn, model, nil
393 }
394}
395
Scott Baker5201c0b2019-05-15 15:35:56 -0700396func ItemsToDynamicMessageList(items interface{}) []*dynamic.Message {
397 result := make([]*dynamic.Message, len(items.([]interface{})))
398 for i, item := range items.([]interface{}) {
399 result[i] = item.(*dynamic.Message)
400 }
401 return result
402}
403
404// List all objects of a given model
405func ListModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string) ([]*dynamic.Message, error) {
Scott Baker6cf525a2019-05-09 12:25:08 -0700406 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
407 defer cancel()
408
409 headers := GenerateHeaders()
410
Scott Baker6cf525a2019-05-09 12:25:08 -0700411 h := &RpcEventHandler{}
412 err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.List"+modelName, headers, h, h.GetParams)
413 if err != nil {
414 return nil, err
415 }
416
417 if h.Status != nil && h.Status.Err() != nil {
418 return nil, h.Status.Err()
419 }
420
421 d, err := dynamic.AsDynamicMessage(h.Response)
422 if err != nil {
423 return nil, err
424 }
425
426 items, err := d.TryGetFieldByName("items")
427 if err != nil {
428 return nil, err
429 }
430
Scott Baker5201c0b2019-05-15 15:35:56 -0700431 return ItemsToDynamicMessageList(items), nil
432}
Scott Baker6cf525a2019-05-09 12:25:08 -0700433
Scott Baker5201c0b2019-05-15 15:35:56 -0700434// Filter models based on field values
435// queries is a map of <field_name> to <operator><query>
436// For example,
437// map[string]string{"name": "==mysite"}
438func FilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
439 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
440 defer cancel()
Scott Baker6cf525a2019-05-09 12:25:08 -0700441
Scott Baker5201c0b2019-05-15 15:35:56 -0700442 headers := GenerateHeaders()
443
444 model_descriptor, err := descriptor.FindSymbol("xos." + modelName)
445 if err != nil {
446 return nil, err
447 }
448 model_md, ok := model_descriptor.(*desc.MessageDescriptor)
449 if !ok {
450 return nil, errors.New("Failed to convert model to a messagedescriptor")
Scott Baker6cf525a2019-05-09 12:25:08 -0700451 }
452
Scott Baker5201c0b2019-05-15 15:35:56 -0700453 h := &QueryEventHandler{
454 RpcEventHandler: RpcEventHandler{
455 Fields: map[string]map[string]interface{}{"xos.Query": map[string]interface{}{"kind": 0}},
456 },
457 Elements: queries,
458 Model: model_md,
459 Kind: "DEFAULT",
460 }
461 err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Filter"+modelName, headers, h, h.GetParams)
462 if err != nil {
463 return nil, err
464 }
465
466 if h.Status != nil && h.Status.Err() != nil {
467 return nil, h.Status.Err()
468 }
469
470 d, err := dynamic.AsDynamicMessage(h.Response)
471 if err != nil {
472 return nil, err
473 }
474
475 items, err := d.TryGetFieldByName("items")
476 if err != nil {
477 return nil, err
478 }
479
480 return ItemsToDynamicMessageList(items), nil
481}
482
Scott Baker5281d002019-05-16 10:45:26 -0700483// Call ListModels or FilterModels as appropriate
484func ListOrFilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
485 if len(queries) == 0 {
486 return ListModels(conn, descriptor, modelName)
487 } else {
488 return FilterModels(conn, descriptor, modelName, queries)
489 }
490}
491
Scott Baker5201c0b2019-05-15 15:35:56 -0700492// Get a model from XOS given a fieldName/fieldValue
493func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
494 models, err := FilterModels(conn, descriptor, modelName, queries)
495 if err != nil {
496 return nil, err
497 }
498
499 if len(models) == 0 {
500 return nil, errors.New("rpc error: code = NotFound")
501 }
502
503 return models[0], nil
Scott Baker6cf525a2019-05-09 12:25:08 -0700504}
505
506// Find a model, but retry under a variety of circumstances
Scott Baker5201c0b2019-05-15 15:35:56 -0700507func FindModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
Scott Baker6cf525a2019-05-09 12:25:08 -0700508 quiet := (flags & GM_QUIET) != 0
509 until_found := (flags & GM_UNTIL_FOUND) != 0
510 until_enacted := (flags & GM_UNTIL_ENACTED) != 0
511 until_status := (flags & GM_UNTIL_STATUS) != 0
512
513 for {
514 var err error
515
516 if conn == nil {
517 conn, err = NewConnection()
518 if err != nil {
519 return nil, nil, err
520 }
521 }
522
Scott Baker5201c0b2019-05-15 15:35:56 -0700523 model, err := FindModel(conn, descriptor, modelName, queries)
Scott Baker6cf525a2019-05-09 12:25:08 -0700524 if err != nil {
525 if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
526 strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
527 if !quiet {
528 fmt.Print(".")
529 }
530 time.Sleep(100 * time.Millisecond)
531 conn.Close()
532 conn = nil
533 continue
534 }
535
536 if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
537 if !quiet {
538 fmt.Print("x")
539 }
540 time.Sleep(100 * time.Millisecond)
541 continue
542 }
543 return nil, nil, err
544 }
545
546 if until_enacted && !IsEnacted(model) {
547 if !quiet {
548 fmt.Print("o")
549 }
550 time.Sleep(100 * time.Millisecond)
551 continue
552 }
553
554 if until_status && model.GetFieldByName("status") == nil {
555 if !quiet {
556 fmt.Print("O")
557 }
558 time.Sleep(100 * time.Millisecond)
559 continue
560 }
561
562 return conn, model, nil
563 }
564}
565
Scott Baker175cb402019-05-17 16:13:06 -0700566// Get a model from XOS given its ID
567func DeleteModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) error {
568 ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
569 defer cancel()
570
571 headers := GenerateHeaders()
572
573 h := &RpcEventHandler{
574 Fields: map[string]map[string]interface{}{"xos.ID": map[string]interface{}{"id": id}},
575 }
576 err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Delete"+modelName, headers, h, h.GetParams)
577 if err != nil {
578 return err
579 }
580
581 if h.Status != nil && h.Status.Err() != nil {
582 return h.Status.Err()
583 }
584
585 _, err = dynamic.AsDynamicMessage(h.Response)
586 if err != nil {
587 return err
588 }
589
590 return nil
591}
592
Scott Baker5201c0b2019-05-15 15:35:56 -0700593// Takes a *dynamic.Message and turns it into a map of fields to interfaces
594// TODO: Might be more useful to convert the values to strings and ints
Scott Baker6cf525a2019-05-09 12:25:08 -0700595func MessageToMap(d *dynamic.Message) map[string]interface{} {
596 fields := make(map[string]interface{})
597 for _, field_desc := range d.GetKnownFields() {
598 field_name := field_desc.GetName()
599 fields[field_name] = d.GetFieldByName(field_name)
600 }
601 return fields
602}
603
Scott Baker5201c0b2019-05-15 15:35:56 -0700604// Returns True if a message has been enacted
Scott Baker6cf525a2019-05-09 12:25:08 -0700605func IsEnacted(d *dynamic.Message) bool {
606 enacted := d.GetFieldByName("enacted").(float64)
607 updated := d.GetFieldByName("updated").(float64)
608
609 return (enacted >= updated)
610}