blob: b5378fe670f2827a974470a49482c19940ab34a7 [file] [log] [blame]
Scott Baker2c1c4822019-10-16 11:02:41 -07001/*
2 * Copyright 2018-present Open Networking Foundation
3
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7
8 * http://www.apache.org/licenses/LICENSE-2.0
9
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package model
18
19import (
20 "context"
21 "crypto/md5"
22 "errors"
23 "fmt"
24 "github.com/google/uuid"
Scott Bakerce767002019-10-23 13:30:24 -070025 "github.com/opencord/voltha-lib-go/v2/pkg/log"
Scott Baker2c1c4822019-10-16 11:02:41 -070026 "reflect"
27 "runtime"
28 "strings"
29 "sync"
30)
31
32// OperationContext holds details on the information used during an operation
33type OperationContext struct {
34 Path string
35 Data interface{}
36 FieldName string
37 ChildKey string
38}
39
40// NewOperationContext instantiates a new OperationContext structure
41func NewOperationContext(path string, data interface{}, fieldName string, childKey string) *OperationContext {
42 oc := &OperationContext{
43 Path: path,
44 Data: data,
45 FieldName: fieldName,
46 ChildKey: childKey,
47 }
48 return oc
49}
50
51// Update applies new data to the context structure
52func (oc *OperationContext) Update(data interface{}) *OperationContext {
53 oc.Data = data
54 return oc
55}
56
57// Proxy holds the information for a specific location with the data model
58type Proxy struct {
59 mutex sync.RWMutex
60 Root *root
61 Node *node
62 ParentNode *node
63 Path string
64 FullPath string
65 Exclusive bool
66 Callbacks map[CallbackType]map[string]*CallbackTuple
67 operation ProxyOperation
68}
69
70// NewProxy instantiates a new proxy to a specific location
71func NewProxy(root *root, node *node, parentNode *node, path string, fullPath string, exclusive bool) *Proxy {
72 callbacks := make(map[CallbackType]map[string]*CallbackTuple)
73 if fullPath == "/" {
74 fullPath = ""
75 }
76 p := &Proxy{
77 Root: root,
78 Node: node,
79 ParentNode: parentNode,
80 Exclusive: exclusive,
81 Path: path,
82 FullPath: fullPath,
83 Callbacks: callbacks,
84 }
85 return p
86}
87
88// GetRoot returns the root attribute of the proxy
89func (p *Proxy) GetRoot() *root {
90 return p.Root
91}
92
93// getPath returns the path attribute of the proxy
94func (p *Proxy) getPath() string {
95 return p.Path
96}
97
98// getFullPath returns the full path attribute of the proxy
99func (p *Proxy) getFullPath() string {
100 return p.FullPath
101}
102
103// getCallbacks returns the full list of callbacks associated to the proxy
104func (p *Proxy) getCallbacks(callbackType CallbackType) map[string]*CallbackTuple {
105 p.mutex.RLock()
106 defer p.mutex.RUnlock()
107
108 if p != nil {
109 if cb, exists := p.Callbacks[callbackType]; exists {
110 return cb
111 }
112 } else {
113 log.Debugw("proxy-is-nil", log.Fields{"callback-type": callbackType.String()})
114 }
115 return nil
116}
117
118// getCallback returns a specific callback matching the type and function hash
119func (p *Proxy) getCallback(callbackType CallbackType, funcHash string) *CallbackTuple {
120 p.mutex.Lock()
121 defer p.mutex.Unlock()
122 if tuple, exists := p.Callbacks[callbackType][funcHash]; exists {
123 return tuple
124 }
125 return nil
126}
127
128// setCallbacks applies a callbacks list to a type
129func (p *Proxy) setCallbacks(callbackType CallbackType, callbacks map[string]*CallbackTuple) {
130 p.mutex.Lock()
131 defer p.mutex.Unlock()
132 p.Callbacks[callbackType] = callbacks
133}
134
135// setCallback applies a callback to a type and hash value
136func (p *Proxy) setCallback(callbackType CallbackType, funcHash string, tuple *CallbackTuple) {
137 p.mutex.Lock()
138 defer p.mutex.Unlock()
139 p.Callbacks[callbackType][funcHash] = tuple
140}
141
142// DeleteCallback removes a callback matching the type and hash
143func (p *Proxy) DeleteCallback(callbackType CallbackType, funcHash string) {
144 p.mutex.Lock()
145 defer p.mutex.Unlock()
146 delete(p.Callbacks[callbackType], funcHash)
147}
148
149// CallbackType is an enumerated value to express when a callback should be executed
150type ProxyOperation uint8
151
152// Enumerated list of callback types
153const (
154 PROXY_NONE ProxyOperation = iota
155 PROXY_GET
156 PROXY_LIST
157 PROXY_ADD
158 PROXY_UPDATE
159 PROXY_REMOVE
160 PROXY_CREATE
161 PROXY_WATCH
162)
163
164var proxyOperationTypes = []string{
165 "PROXY_NONE",
166 "PROXY_GET",
167 "PROXY_LIST",
168 "PROXY_ADD",
169 "PROXY_UPDATE",
170 "PROXY_REMOVE",
171 "PROXY_CREATE",
172 "PROXY_WATCH",
173}
174
175func (t ProxyOperation) String() string {
176 return proxyOperationTypes[t]
177}
178
179func (p *Proxy) GetOperation() ProxyOperation {
180 p.mutex.RLock()
181 defer p.mutex.RUnlock()
182 return p.operation
183}
184
185func (p *Proxy) SetOperation(operation ProxyOperation) {
186 p.mutex.Lock()
187 defer p.mutex.Unlock()
188 p.operation = operation
189}
190
191// parseForControlledPath verifies if a proxy path matches a pattern
192// for locations that need to be access controlled.
193func (p *Proxy) parseForControlledPath(path string) (pathLock string, controlled bool) {
194 // TODO: Add other path prefixes that may need control
195 if strings.HasPrefix(path, "/devices") ||
196 strings.HasPrefix(path, "/logical_devices") ||
197 strings.HasPrefix(path, "/adapters") {
198
199 split := strings.SplitN(path, "/", -1)
200 switch len(split) {
201 case 2:
202 controlled = false
203 pathLock = ""
204 break
205 case 3:
206 fallthrough
207 default:
208 pathLock = fmt.Sprintf("%s/%s", split[1], split[2])
209 controlled = true
210 }
211 }
212 return pathLock, controlled
213}
214
215// List will retrieve information from the data model at the specified path location
216// A list operation will force access to persistence storage
217func (p *Proxy) List(ctx context.Context, path string, depth int, deep bool, txid string) interface{} {
218 var effectivePath string
219 if path == "/" {
220 effectivePath = p.getFullPath()
221 } else {
222 effectivePath = p.getFullPath() + path
223 }
224
225 pathLock, controlled := p.parseForControlledPath(effectivePath)
226
227 p.SetOperation(PROXY_LIST)
228 defer p.SetOperation(PROXY_NONE)
229
230 log.Debugw("proxy-list", log.Fields{
231 "path": path,
232 "effective": effectivePath,
233 "pathLock": pathLock,
234 "controlled": controlled,
235 "operation": p.GetOperation(),
236 })
237
238 rv := p.GetRoot().List(ctx, path, "", depth, deep, txid)
239
240 return rv
241}
242
243// Get will retrieve information from the data model at the specified path location
244func (p *Proxy) Get(ctx context.Context, path string, depth int, deep bool, txid string) interface{} {
245 var effectivePath string
246 if path == "/" {
247 effectivePath = p.getFullPath()
248 } else {
249 effectivePath = p.getFullPath() + path
250 }
251
252 pathLock, controlled := p.parseForControlledPath(effectivePath)
253
254 p.SetOperation(PROXY_GET)
255 defer p.SetOperation(PROXY_NONE)
256
257 log.Debugw("proxy-get", log.Fields{
258 "path": path,
259 "effective": effectivePath,
260 "pathLock": pathLock,
261 "controlled": controlled,
262 "operation": p.GetOperation(),
263 })
264
265 rv := p.GetRoot().Get(ctx, path, "", depth, deep, txid)
266
267 return rv
268}
269
270// Update will modify information in the data model at the specified location with the provided data
271func (p *Proxy) Update(ctx context.Context, path string, data interface{}, strict bool, txid string) interface{} {
272 if !strings.HasPrefix(path, "/") {
273 log.Errorf("invalid path: %s", path)
274 return nil
275 }
276 var fullPath string
277 var effectivePath string
278 if path == "/" {
279 fullPath = p.getPath()
280 effectivePath = p.getFullPath()
281 } else {
282 fullPath = p.getPath() + path
283 effectivePath = p.getFullPath() + path
284 }
285
286 pathLock, controlled := p.parseForControlledPath(effectivePath)
287
288 p.SetOperation(PROXY_UPDATE)
289 defer p.SetOperation(PROXY_NONE)
290
291 log.Debugw("proxy-update", log.Fields{
292 "path": path,
293 "effective": effectivePath,
294 "full": fullPath,
295 "pathLock": pathLock,
296 "controlled": controlled,
297 "operation": p.GetOperation(),
298 })
299
300 if p.GetRoot().KvStore != nil {
301 p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
302 defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
303 }
304
305 result := p.GetRoot().Update(ctx, fullPath, data, strict, txid, nil)
306
307 if result != nil {
308 return result.GetData()
309 }
310
311 return nil
312}
313
314// AddWithID will insert new data at specified location.
315// This method also allows the user to specify the ID of the data entry to ensure
316// that access control is active while inserting the information.
317func (p *Proxy) AddWithID(ctx context.Context, path string, id string, data interface{}, txid string) interface{} {
318 if !strings.HasPrefix(path, "/") {
319 log.Errorf("invalid path: %s", path)
320 return nil
321 }
322 var fullPath string
323 var effectivePath string
324 if path == "/" {
325 fullPath = p.getPath()
326 effectivePath = p.getFullPath()
327 } else {
328 fullPath = p.getPath() + path
329 effectivePath = p.getFullPath() + path + "/" + id
330 }
331
332 pathLock, controlled := p.parseForControlledPath(effectivePath)
333
334 p.SetOperation(PROXY_ADD)
335 defer p.SetOperation(PROXY_NONE)
336
337 log.Debugw("proxy-add-with-id", log.Fields{
338 "path": path,
339 "effective": effectivePath,
340 "full": fullPath,
341 "pathLock": pathLock,
342 "controlled": controlled,
343 "operation": p.GetOperation(),
344 })
345
346 if p.GetRoot().KvStore != nil {
347 p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
348 defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
349 }
350
351 result := p.GetRoot().Add(ctx, fullPath, data, txid, nil)
352
353 if result != nil {
354 return result.GetData()
355 }
356
357 return nil
358}
359
360// Add will insert new data at specified location.
361func (p *Proxy) Add(ctx context.Context, path string, data interface{}, txid string) interface{} {
362 if !strings.HasPrefix(path, "/") {
363 log.Errorf("invalid path: %s", path)
364 return nil
365 }
366 var fullPath string
367 var effectivePath string
368 if path == "/" {
369 fullPath = p.getPath()
370 effectivePath = p.getFullPath()
371 } else {
372 fullPath = p.getPath() + path
373 effectivePath = p.getFullPath() + path
374 }
375
376 pathLock, controlled := p.parseForControlledPath(effectivePath)
377
378 p.SetOperation(PROXY_ADD)
379 defer p.SetOperation(PROXY_NONE)
380
381 log.Debugw("proxy-add", log.Fields{
382 "path": path,
383 "effective": effectivePath,
384 "full": fullPath,
385 "pathLock": pathLock,
386 "controlled": controlled,
387 "operation": p.GetOperation(),
388 })
389
390 if p.GetRoot().KvStore != nil {
391 p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
392 defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
393 }
394
395 result := p.GetRoot().Add(ctx, fullPath, data, txid, nil)
396
397 if result != nil {
398 return result.GetData()
399 }
400
401 return nil
402}
403
404// Remove will delete an entry at the specified location
405func (p *Proxy) Remove(ctx context.Context, path string, txid string) interface{} {
406 if !strings.HasPrefix(path, "/") {
407 log.Errorf("invalid path: %s", path)
408 return nil
409 }
410 var fullPath string
411 var effectivePath string
412 if path == "/" {
413 fullPath = p.getPath()
414 effectivePath = p.getFullPath()
415 } else {
416 fullPath = p.getPath() + path
417 effectivePath = p.getFullPath() + path
418 }
419
420 pathLock, controlled := p.parseForControlledPath(effectivePath)
421
422 p.SetOperation(PROXY_REMOVE)
423 defer p.SetOperation(PROXY_NONE)
424
425 log.Debugw("proxy-remove", log.Fields{
426 "path": path,
427 "effective": effectivePath,
428 "full": fullPath,
429 "pathLock": pathLock,
430 "controlled": controlled,
431 "operation": p.GetOperation(),
432 })
433
434 if p.GetRoot().KvStore != nil {
435 p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
436 defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
437 }
438
439 result := p.GetRoot().Remove(ctx, fullPath, txid, nil)
440
441 if result != nil {
442 return result.GetData()
443 }
444
445 return nil
446}
447
448// CreateProxy to interact with specific path directly
449func (p *Proxy) CreateProxy(ctx context.Context, path string, exclusive bool) *Proxy {
450 if !strings.HasPrefix(path, "/") {
451 log.Errorf("invalid path: %s", path)
452 return nil
453 }
454
455 var fullPath string
456 var effectivePath string
457 if path == "/" {
458 fullPath = p.getPath()
459 effectivePath = p.getFullPath()
460 } else {
461 fullPath = p.getPath() + path
462 effectivePath = p.getFullPath() + path
463 }
464
465 pathLock, controlled := p.parseForControlledPath(effectivePath)
466
467 p.SetOperation(PROXY_CREATE)
468 defer p.SetOperation(PROXY_NONE)
469
470 log.Debugw("proxy-create", log.Fields{
471 "path": path,
472 "effective": effectivePath,
473 "full": fullPath,
474 "pathLock": pathLock,
475 "controlled": controlled,
476 "operation": p.GetOperation(),
477 })
478
479 if p.GetRoot().KvStore != nil {
480 p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
481 defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
482 }
483
484 return p.GetRoot().CreateProxy(ctx, fullPath, exclusive)
485}
486
487// OpenTransaction creates a new transaction branch to isolate operations made to the data model
488func (p *Proxy) OpenTransaction() *Transaction {
489 txid := p.GetRoot().MakeTxBranch()
490 return NewTransaction(p, txid)
491}
492
493// commitTransaction will apply and merge modifications made in the transaction branch to the data model
494func (p *Proxy) commitTransaction(txid string) {
495 p.GetRoot().FoldTxBranch(txid)
496}
497
498// cancelTransaction will terminate a transaction branch along will all changes within it
499func (p *Proxy) cancelTransaction(txid string) {
500 p.GetRoot().DeleteTxBranch(txid)
501}
502
503// CallbackFunction is a type used to define callback functions
504type CallbackFunction func(args ...interface{}) interface{}
505
506// CallbackTuple holds the function and arguments details of a callback
507type CallbackTuple struct {
508 callback CallbackFunction
509 args []interface{}
510}
511
512// Execute will process the a callback with its provided arguments
513func (tuple *CallbackTuple) Execute(contextArgs []interface{}) interface{} {
514 args := []interface{}{}
515
516 for _, ta := range tuple.args {
517 args = append(args, ta)
518 }
519
520 if contextArgs != nil {
521 for _, ca := range contextArgs {
522 args = append(args, ca)
523 }
524 }
525
526 return tuple.callback(args...)
527}
528
529// RegisterCallback associates a callback to the proxy
530func (p *Proxy) RegisterCallback(callbackType CallbackType, callback CallbackFunction, args ...interface{}) {
531 if p.getCallbacks(callbackType) == nil {
532 p.setCallbacks(callbackType, make(map[string]*CallbackTuple))
533 }
534 funcName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
535 log.Debugf("value of function: %s", funcName)
536 funcHash := fmt.Sprintf("%x", md5.Sum([]byte(funcName)))[:12]
537
538 p.setCallback(callbackType, funcHash, &CallbackTuple{callback, args})
539}
540
541// UnregisterCallback removes references to a callback within a proxy
542func (p *Proxy) UnregisterCallback(callbackType CallbackType, callback CallbackFunction, args ...interface{}) {
543 if p.getCallbacks(callbackType) == nil {
544 log.Errorf("no such callback type - %s", callbackType.String())
545 return
546 }
547
548 funcName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
549 funcHash := fmt.Sprintf("%x", md5.Sum([]byte(funcName)))[:12]
550
551 log.Debugf("value of function: %s", funcName)
552
553 if p.getCallback(callbackType, funcHash) == nil {
554 log.Errorf("function with hash value: '%s' not registered with callback type: '%s'", funcHash, callbackType)
555 return
556 }
557
558 p.DeleteCallback(callbackType, funcHash)
559}
560
561func (p *Proxy) invoke(callback *CallbackTuple, context []interface{}) (result interface{}, err error) {
562 defer func() {
563 if r := recover(); r != nil {
564 errStr := fmt.Sprintf("callback error occurred: %+v", r)
565 err = errors.New(errStr)
566 log.Error(errStr)
567 }
568 }()
569
570 result = callback.Execute(context)
571
572 return result, err
573}
574
575// InvokeCallbacks executes all callbacks associated to a specific type
576func (p *Proxy) InvokeCallbacks(args ...interface{}) (result interface{}) {
577 callbackType := args[0].(CallbackType)
578 proceedOnError := args[1].(bool)
579 context := args[2:]
580
581 var err error
582
583 if callbacks := p.getCallbacks(callbackType); callbacks != nil {
584 p.mutex.Lock()
585 for _, callback := range callbacks {
586 if result, err = p.invoke(callback, context); err != nil {
587 if !proceedOnError {
588 log.Info("An error occurred. Stopping callback invocation")
589 break
590 }
591 log.Info("An error occurred. Invoking next callback")
592 }
593 }
594 p.mutex.Unlock()
595 }
596
597 return result
598}