Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-2320] Reduce cyclomatic complexity of the browse function #119

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
149 changes: 8 additions & 141 deletions opcua_plugin/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,157 +133,24 @@ func browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
return
}

var newPath string
if path == "" {
newPath = sanitize(browseName.Name)
} else {
newPath = path + "." + sanitize(browseName.Name)
newPath := sanitize(browseName.Name)
if path != "" {
newPath = path + "." + newPath
}

var def = NodeDef{
NodeID: n.ID(),
Path: newPath,
NodeID: n.ID(),
Path: newPath,
ParentNodeID: parentNodeId,
}

switch err := attrs[0].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[0].Value == nil {
sendError(ctx, errors.New("node class is nil"), errChan, logger)
return
} else {
def.NodeClass = ua.NodeClass(attrs[0].Value.Int())
}
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the NodeClass, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the NodeClass for browsing
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[1].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[1].Value == nil {
sendError(ctx, errors.New("browse name is nil"), errChan, logger)
return
} else {
def.BrowseName = attrs[1].Value.String()
}
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the BrowseName, skipping...\n", path)
return // We need to return here, as we can't continue without the BrowseName (we need it at least for the path when browsing the children)
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[2].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[2].Value == nil {
def.Description = "" // this can happen for example in Kepware v6, where the description is OPCUAType_Null
} else {
def.Description = attrs[2].Value.String()
}
case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the Description, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the Description
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[3].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[3].Value == nil {
sendError(ctx, errors.New("access level is nil"), errChan, logger)
return
} else {
def.AccessLevel = ua.AccessLevelType(attrs[3].Value.Int())
}
case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the AccessLevel, continuing...\n", path)
// no need to return here, as we can continue without the AccessLevel for browsing
default:
sendError(ctx, err, errChan, logger)
return
}

// if AccessLevel exists and it is set to None
if def.AccessLevel == ua.AccessLevelTypeNone && errors.Is(err, ua.StatusOK) {
logger.Warnf("Tried to browse node: %s but access level is None ('access denied'). Do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// we need to continue here, as we still want to browse the children of this node
}

switch err := attrs[4].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[4].Value == nil {
// This is not an error, it can happen for some OPC UA servers...
// in oru case it is the amine amaach opcua simulator
// if the data type is nil, we simpy ignore it
// errChan <- errors.New("data type is nil")
logger.Debugf("ignoring node: %s as its datatype is nil...\n", path)
return
} else {
switch v := attrs[4].Value.NodeID().IntID(); v {
case id.DateTime:
def.DataType = "time.Time"
case id.Boolean:
def.DataType = "bool"
case id.SByte:
def.DataType = "int8"
case id.Int16:
def.DataType = "int16"
case id.Int32:
def.DataType = "int32"
case id.Byte:
def.DataType = "byte"
case id.UInt16:
def.DataType = "uint16"
case id.UInt32:
def.DataType = "uint32"
case id.UtcTime:
def.DataType = "time.Time"
case id.String:
def.DataType = "string"
case id.Float:
def.DataType = "float32"
case id.Double:
def.DataType = "float64"
default:
def.DataType = attrs[4].Value.NodeID().String()
}
}

case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the DataType, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the DataType
default:
// def.NodeClass, def.BrowseName, def.Description, def.AccessLevel, def.DataType are set in the processNodeAttributes function
if err := processNodeAttributes(attrs, &def, newPath, logger); err != nil {
sendError(ctx, err, errChan, logger)
return
}

logger.Debugf("%d: def.Path:%s def.NodeClass:%s\n", level, def.Path, def.NodeClass)
def.ParentNodeID = parentNodeId

browseChildrenV2 := func(refType uint32) error {
children, err := n.Children(ctx, refType, ua.NodeClassVariable|ua.NodeClassObject)
Expand Down
177 changes: 177 additions & 0 deletions opcua_plugin/node_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package opcua_plugin

import (
"fmt"

"github.com/gopcua/opcua/id"
"github.com/gopcua/opcua/ua"
)

// AttributeHandler is the struct that handles the attributes of a node
type AttributeHandler struct {
// Using more idiomatic names
handleOK func(value *ua.Variant) error // handleOK is the function that handles the OK status
handleNotReadable func() // handleNotReadable is the function that handles the NotReadable status
ignoreInvalidAttr bool // ignoreInvalidAttr is the flag that determines if invalid attributes should be ignored
requiresValue bool // requiresValue is the flag that determines if a value is required
affectsNodeClass bool // affectsNodeClass is the flag that determines if the node class should be affected
}

// handleAttributeStatus is the function that handles the attributes of a node
func handleAttributeStatus(
name string,
attr *ua.DataValue,
def *NodeDef,
path string,
logger Logger,
handler AttributeHandler,
) error {
if attr == nil {
return fmt.Errorf("attribute is nil for node: %s and attribute: %s", def.NodeID, name)
}

if attr.Status == ua.StatusOK {
if attr.Value == nil && handler.requiresValue {
return fmt.Errorf("attribute value is nil for node: %s and attribute: %s", def.NodeID, name)
}
return handleOKStatus(attr.Value, handler)
}

if attr.Status == ua.StatusBadSecurityModeInsufficient {
return fmt.Errorf("got insufficient security mode for node: %s and attribute: %s", def.NodeID, name)
}

if attr.Status == ua.StatusBadAttributeIDInvalid {
if handler.ignoreInvalidAttr {
return nil
}
return fmt.Errorf("invalid attribute ID for node: %s and attribute: %s", def.NodeID, name)
}

if attr.Status == ua.StatusBadNotReadable {
handleNotReadable(def, handler, path, logger)
return nil
}

return attr.Status
}

// handleOKStatus processes successful attribute reads
func handleOKStatus(value *ua.Variant, handler AttributeHandler) error {
if handler.handleOK != nil && value != nil {
return handler.handleOK(value)
}
return nil
}

// handleNotReadable processes non-readable attributes
func handleNotReadable(def *NodeDef, handler AttributeHandler, path string, logger Logger) {
if handler.affectsNodeClass {
def.NodeClass = ua.NodeClassObject
}
if handler.handleNotReadable != nil {
handler.handleNotReadable()
}
logger.Warnf("Access denied for node: %s, continuing...\n", path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be careful about warning, they will prevent deployment in the management console

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code in master branch uses logger.Warnf for this scenario and the status quo is maintained.

}

// processNodeAttributes processes all attributes for a node
// This function is used to process the attributes of a node and set the NodeDef struct
// def.NodeClass, def.BrowseName, def.Description, def.AccessLevel, def.DataType are set in the processNodeAttributes function
func processNodeAttributes(attrs []*ua.DataValue, def *NodeDef, path string, logger Logger) error {
// NodeClass (attrs[0])
nodeClassHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.NodeClass = ua.NodeClass(value.Int())
return nil
},
requiresValue: true, // NodeClass is required else the node is not valid
affectsNodeClass: true, // If the node class could not be determined, it is set to Object type and browsing can be continued
}
if err := handleAttributeStatus("NodeClass", attrs[0], def, path, logger, nodeClassHandler); err != nil {
return err
}

// BrowseName (attrs[1])
browseNameHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.BrowseName = value.String()
return nil
},
requiresValue: true, // BrowseName is required else the node is not valid
}
if err := handleAttributeStatus("BrowseName", attrs[1], def, path, logger, browseNameHandler); err != nil {
return err
}

// Description (attrs[2])
descriptionHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
if value != nil {
def.Description = value.String()
return nil
}
// Description for Kepware v6 is OPCUATYPE_NULL. Set to empty string in such cases
def.Description = ""
return nil
},
ignoreInvalidAttr: true, // For some node, Description is not available and this is not an error and can be ignored
affectsNodeClass: true, // If the node class could not be determined, it is set to Object type and browsing can be continued. By setting it to object, we will not subscribe to this node
}
if err := handleAttributeStatus("Description", attrs[2], def, path, logger, descriptionHandler); err != nil {
return err
}

// AccessLevel (attrs[3])
accessLevelHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.AccessLevel = ua.AccessLevelType(value.Int())
return nil
},
ignoreInvalidAttr: true, // For some node, AccessLevel is not available and this is not an error and can be ignored
}
if err := handleAttributeStatus("AccessLevel", attrs[3], def, path, logger, accessLevelHandler); err != nil {
return err
}

// DataType (attrs[4])
dataTypeHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
if value == nil {
logger.Debugf("ignoring node: %s as its datatype is nil...\n", path)
return fmt.Errorf("datatype is nil for node: %s and attribute: %s", def.NodeID, "DataType")
}
def.DataType = getDataTypeString(value.NodeID().IntID())
return nil
},
ignoreInvalidAttr: true, // For some node, DataType is not available and this is not an error and can be ignored
affectsNodeClass: true, // If the node class could not be determined, it is set to Object type and browsing can be continued. By setting it to object, we will not subscribe to this node
}
if err := handleAttributeStatus("DataType", attrs[4], def, path, logger, dataTypeHandler); err != nil {
return err
}

return nil
}

var dataTypeMap = map[uint32]string{
id.DateTime: "time.Time",
id.Boolean: "bool",
id.SByte: "int8",
id.Int16: "int16",
id.Int32: "int32",
id.Byte: "byte",
id.UInt16: "uint16",
id.UInt32: "uint32",
id.UtcTime: "time.Time",
id.String: "string",
id.Float: "float32",
id.Double: "float64",
}

func getDataTypeString(typeID uint32) string {
if dtype, ok := dataTypeMap[typeID]; ok {
return dtype
}
return fmt.Sprintf("ns=%d;i=%d", 0, typeID)
}
2 changes: 1 addition & 1 deletion opcua_plugin/opcua_unittest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ var _ = Describe("Unit Tests", func() {
}

Expect(errs).Should(HaveLen(1))
Expect(errs[0].Error()).To(Equal("opcua: node class is nil"))
Expect(errs[0].Error()).To(Equal("attribute value is nil for node: i=1234 and attribute: NodeClass"))
})

It("should browse the root node with no children with right attributes", func() {
Expand Down
Loading