Test Failed
Pull Request — master (#605)
by Olivier
03:47
created

NodeManagementService.delete_references()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 5
cts 5
cp 1
crap 2
rs 9.4285
1 1
from threading import RLock
2 1
import logging
3 1
from datetime import datetime
4 1
import collections
5 1
import shelve
6 1
try:
7 1
    import cPickle as pickle
8 1
except:
9 1
    import pickle
10
11 1
from opcua import ua
12 1
from opcua.server.users import User
13
14
15 1
class AttributeValue(object):
16
17 1
    def __init__(self, value):
18 1
        self.value = value
19 1
        self.value_callback = None
20 1
        self.datachange_callbacks = {}
21
22 1
    def __str__(self):
23
        return "AttributeValue({0})".format(self.value)
24 1
    __repr__ = __str__
25
26
27 1
class NodeData(object):
28
29 1
    def __init__(self, nodeid):
30 1
        self.nodeid = nodeid
31 1
        self.attributes = {}
32 1
        self.references = []
33 1
        self.call = None
34
35 1
    def __str__(self):
36
        return "NodeData(id:{0}, attrs:{1}, refs:{2})".format(self.nodeid, self.attributes, self.references)
37 1
    __repr__ = __str__
38
39
40 1
class AttributeService(object):
41
42 1
    def __init__(self, aspace):
43 1
        self.logger = logging.getLogger(__name__)
44 1
        self._aspace = aspace
45
46 1
    def read(self, params):
47 1
        self.logger.debug("read %s", params)
48 1
        res = []
49 1
        for readvalue in params.NodesToRead:
50 1
            res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
51 1
        return res
52
53 1
    def write(self, params, user=User.Admin):
54 1
        self.logger.debug("write %s as user %s", params, user)
55 1
        res = []
56 1
        for writevalue in params.NodesToWrite:
57 1
            if user != User.Admin:
58 1
                if writevalue.AttributeId != ua.AttributeIds.Value:
59 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
60 1
                    continue
61 1
                al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
62 1
                ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
63 1
                if not ua.ua_binary.test_bit(al.Value.Value, ua.AccessLevel.CurrentWrite) or not ua.ua_binary.test_bit(ual.Value.Value, ua.AccessLevel.CurrentWrite):
64 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
65 1
                    continue
66 1
            res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
67 1
        return res
68
69
70 1
class ViewService(object):
71
72 1
    def __init__(self, aspace):
73 1
        self.logger = logging.getLogger(__name__)
74 1
        self._aspace = aspace
75
76 1
    def browse(self, params):
77 1
        self.logger.debug("browse %s", params)
78 1
        res = []
79 1
        for desc in params.NodesToBrowse:
80 1
            res.append(self._browse(desc))
81 1
        return res
82
83 1
    def _browse(self, desc):
84 1
        res = ua.BrowseResult()
85 1
        if desc.NodeId not in self._aspace:
86 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
87 1
            return res
88 1
        node = self._aspace[desc.NodeId]
89 1
        for ref in node.references:
90 1
            if not self._is_suitable_ref(desc, ref):
91 1
                continue
92 1
            res.References.append(ref)
93 1
        return res
94
95 1
    def _is_suitable_ref(self, desc, ref):
96 1
        if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
97 1
            self.logger.debug("%s is not suitable due to direction", ref)
98 1
            return False
99 1
        if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
100 1
            self.logger.debug("%s is not suitable due to type", ref)
101 1
            return False
102 1
        if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
103 1
            self.logger.debug("%s is not suitable due to class", ref)
104 1
            return False
105 1
        self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
106 1
        return True
107
108 1
    def _suitable_reftype(self, ref1, ref2, subtypes):
109
        """
110
        """
111 1
        if ref1 == ua.NodeId(ua.ObjectIds.Null):
112
            # If ReferenceTypeId is not specified in the BrowseDescription,
113
            # all References are returned and includeSubtypes is ignored.
114
            return True
115 1
        if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
116 1
            return False
117 1
        if ref1.Identifier == ref2.Identifier:
118 1
            return True
119 1
        oktypes = self._get_sub_ref(ref1)
120 1
        if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
121 1
            oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
122 1
        return ref2 in oktypes
123
124 1
    def _get_sub_ref(self, ref):
125 1
        res = []
126 1
        nodedata = self._aspace[ref]
127 1
        if nodedata is not None:
128 1
            for ref in nodedata.references:
129 1
                if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
130 1
                    res.append(ref.NodeId)
131 1
                    res += self._get_sub_ref(ref.NodeId)
132 1
        return res
133
134 1
    def _suitable_direction(self, desc, isforward):
135 1
        if desc == ua.BrowseDirection.Both:
136 1
            return True
137 1
        if desc == ua.BrowseDirection.Forward and isforward:
138 1
            return True
139 1
        if desc == ua.BrowseDirection.Inverse and not isforward:
140 1
            return True
141 1
        return False
142
143 1
    def translate_browsepaths_to_nodeids(self, browsepaths):
144 1
        self.logger.debug("translate browsepath: %s", browsepaths)
145 1
        results = []
146 1
        for path in browsepaths:
147 1
            results.append(self._translate_browsepath_to_nodeid(path))
148 1
        return results
149
150 1
    def _translate_browsepath_to_nodeid(self, path):
151 1
        self.logger.debug("looking at path: %s", path)
152 1
        res = ua.BrowsePathResult()
153 1
        if path.StartingNode not in self._aspace:
154 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
155 1
            return res
156 1
        current = path.StartingNode
157 1
        for el in path.RelativePath.Elements:
158 1
            nodeid = self._find_element_in_node(el, current)
159 1
            if not nodeid:
160 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
161 1
                return res
162 1
            current = nodeid
163 1
        target = ua.BrowsePathTarget()
164 1
        target.TargetId = current
165 1
        target.RemainingPathIndex = 4294967295
166 1
        res.Targets = [target]
167 1
        return res
168
169 1
    def _find_element_in_node(self, el, nodeid):
170 1
        nodedata = self._aspace[nodeid]
171 1
        for ref in nodedata.references:
172
            # FIXME: here we should check other arguments!!
173 1
            if ref.BrowseName == el.TargetName:
174 1
                return ref.NodeId
175 1
        self.logger.info("element %s was not found in node %s", el, nodeid)
176 1
        return None
177
178
179 1
class NodeManagementService(object):
180
181 1
    def __init__(self, aspace):
182 1
        self.logger = logging.getLogger(__name__)
183 1
        self._aspace = aspace
184
185 1
    def add_nodes(self, addnodeitems, user=User.Admin):
186 1
        results = []
187 1
        for item in addnodeitems:
188 1
            results.append(self._add_node(item, user))
189 1
        return results
190
191 1
    def try_add_nodes(self, addnodeitems, user=User.Admin, check=True):
192 1
        for item in addnodeitems:
193 1
            ret = self._add_node(item, user, check=check)
194 1
            if not ret.StatusCode.is_good():
195 1
                yield item
196
197 1
    def _add_node(self, item, user, check=True):
198 1
        self.logger.debug("Adding node %s %s", item.RequestedNewNodeId, item.BrowseName)
199 1
        if not user == User.Admin:
200 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
201
            return result
202
203 1
        result = ua.AddNodesResult()
204 1
        if item.RequestedNewNodeId.has_null_identifier():
205
            # If Identifier of requested NodeId is null we generate a new NodeId using
206
            # the namespace of the nodeid, this is an extention of the spec to allow
207
            # to requests the server to generate a new nodeid in a specified namespace
208 1
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
209 1
            item.RequestedNewNodeId = self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex)
210
        else:
211 1
            if item.RequestedNewNodeId in self._aspace:
212 1
                self.logger.warning("AddNodesItem: Requested NodeId %s already exists", item.RequestedNewNodeId)
213 1
                result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
214 1
                return result
215
        
216 1
        if item.ParentNodeId.is_null():
217 1
            self.logger.info("add_node: while adding node %s, requested parent node is null %s %s", item.RequestedNewNodeId, item.ParentNodeId, item.ParentNodeId.is_null())
218 1
            if check:
219 1
                result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
220 1
                return result
221
222 1
        parentdata = self._aspace.get(item.ParentNodeId)
223 1
        if parentdata is None and not item.ParentNodeId.is_null():
224 1
            self.logger.info("add_node: while adding node %s, requested parent node %s does not exists", item.RequestedNewNodeId, item.ParentNodeId)
225 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
226 1
            return result
227
228 1
        nodedata = NodeData(item.RequestedNewNodeId)
229
230 1
        self._add_node_attributes(nodedata, item)
231
232
        # now add our node to db
233 1
        self._aspace[nodedata.nodeid] = nodedata
234
235 1
        if parentdata is not None:
236 1
            self._add_ref_from_parent(nodedata, item, parentdata)
237 1
            self._add_ref_to_parent(nodedata, item, parentdata)
238
239
        # add type definition
240 1
        if item.TypeDefinition != ua.NodeId():
241 1
            self._add_type_definition(nodedata, item)
242
243 1
        result.StatusCode = ua.StatusCode()
244 1
        result.AddedNodeId = nodedata.nodeid
245
246 1
        return result
247
248 1
    def _add_node_attributes(self, nodedata, item):
249
        # add common attrs
250 1
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
251
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
252
        )
253 1
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
254
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
255
        )
256 1
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
257
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
258
        )
259
        # add requested attrs
260 1
        self._add_nodeattributes(item.NodeAttributes, nodedata)
261
262
263 1
    def _add_unique_reference(self, nodedata, desc):
264 1
        for r in nodedata.references:
265 1
            if r.ReferenceTypeId == desc.ReferenceTypeId and r.NodeId == desc.NodeId:
266 1
                if r.IsForward !=  desc.IsForward:
267
                    self.logger.error("Cannot add conflicting reference %s ", str(desc))
268
                    return ua.StatusCode(ua.StatusCodes.BadReferenceNotAllowed)
269 1
                break # ref already exists
270
        else:
271 1
            nodedata.references.append(desc)
272 1
        return ua.StatusCode()
273
274 1
    def _add_ref_from_parent(self, nodedata, item, parentdata):
275 1
        desc = ua.ReferenceDescription()
276 1
        desc.ReferenceTypeId = item.ReferenceTypeId
277 1
        desc.NodeId = nodedata.nodeid
278 1
        desc.NodeClass = item.NodeClass
279 1
        desc.BrowseName = item.BrowseName
280 1
        desc.DisplayName = item.NodeAttributes.DisplayName
281 1
        desc.TypeDefinition = item.TypeDefinition
282 1
        desc.IsForward = True
283 1
        self._add_unique_reference(parentdata, desc)
284
285 1
    def _add_ref_to_parent(self, nodedata, item, parentdata):
286 1
        addref = ua.AddReferencesItem()
287 1
        addref.ReferenceTypeId = item.ReferenceTypeId
288 1
        addref.SourceNodeId = nodedata.nodeid
289 1
        addref.TargetNodeId = item.ParentNodeId
290 1
        addref.TargetNodeClass = parentdata.attributes[ua.AttributeIds.NodeClass].value.Value.Value
291 1
        addref.IsForward = False
292 1
        self._add_reference_no_check(nodedata, addref)
293
294 1
    def _add_type_definition(self, nodedata, item):
295 1
        addref = ua.AddReferencesItem()
296 1
        addref.SourceNodeId = nodedata.nodeid
297 1
        addref.IsForward = True
298 1
        addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
299 1
        addref.TargetNodeId = item.TypeDefinition
300 1
        addref.TargetNodeClass = ua.NodeClass.DataType
301 1
        self._add_reference_no_check(nodedata, addref)
302
303 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
304 1
        results = []
305 1
        for item in deletenodeitems.NodesToDelete:
306 1
            results.append(self._delete_node(item, user))
307 1
        return results
308
309 1
    def _delete_node(self, item, user):
310 1
        if user != User.Admin:
311
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
312
313 1
        if item.NodeId not in self._aspace:
314
            self.logger.warning("DeleteNodesItem: NodeId %s does not exists", item.NodeId)
315
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
316
317 1
        if item.DeleteTargetReferences:
318 1
            for elem in self._aspace.keys():
319 1
                for rdesc in self._aspace[elem].references:
320 1
                    if rdesc.NodeId == item.NodeId:
321 1
                        self._aspace[elem].references.remove(rdesc)
322
323 1
        self._delete_node_callbacks(self._aspace[item.NodeId])
324
325 1
        del(self._aspace[item.NodeId])
326
327 1
        return ua.StatusCode()
328
329 1
    def _delete_node_callbacks(self, nodedata):
330 1
        if ua.AttributeIds.Value in nodedata.attributes:
331 1
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
332
                try:
333
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
334
                    self._aspace.delete_datachange_callback(handle)
335
                except Exception as ex:
336
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
337
338 1
    def add_references(self, refs, user=User.Admin):
339 1
        result = []
340 1
        for ref in refs:
341 1
            result.append(self._add_reference(ref, user))
342 1
        return result
343
344 1
    def try_add_references(self, refs, user=User.Admin):
345 1
        for ref in refs:
346 1
            if not self._add_reference(ref, user).is_good():
347 1
                yield ref
348
349 1
    def _add_reference(self, addref, user):
350 1
        sourcedata = self._aspace.get(addref.SourceNodeId)
351 1
        if sourcedata is None:
352 1
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
353 1
        if addref.TargetNodeId not in self._aspace:
354 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
355 1
        if user != User.Admin:
356
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
357 1
        return self._add_reference_no_check(sourcedata, addref)
358
359 1
    def _add_reference_no_check(self, sourcedata, addref):
360 1
        rdesc = ua.ReferenceDescription()
361 1
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
362 1
        rdesc.IsForward = addref.IsForward
363 1
        rdesc.NodeId = addref.TargetNodeId
364 1
        rdesc.NodeClass = addref.TargetNodeClass
365 1
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
366 1
        if bname:
367 1
            rdesc.BrowseName = bname
368 1
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
369 1
        if dname:
370 1
            rdesc.DisplayName = dname
371 1
        return self._add_unique_reference(sourcedata, rdesc)
372
373 1
    def delete_references(self, refs, user=User.Admin):
374 1
        result = []
375 1
        for ref in refs:
376 1
            result.append(self._delete_reference(ref, user))
377 1
        return result
378
379 1
    def _delete_unique_reference(self, item, invert = False):
380 1
        if invert:
381 1
            source, target, forward = item.TargetNodeId, item.SourceNodeId, not item.IsForward
382
        else:
383 1
            source, target, forward = item.SourceNodeId, item.TargetNodeId, item.IsForward
384 1
        for rdesc in self._aspace[source].references:
385 1
            if rdesc.NodeId == target and rdesc.ReferenceTypeId == item.ReferenceTypeId:
386 1
                if rdesc.IsForward == forward:
387 1
                    self._aspace[source].references.remove(rdesc)
388 1
                    return ua.StatusCode()
389 1
        return ua.StatusCode(ua.StatusCodes.BadNotFound)
390
391 1
    def _delete_reference(self, item, user):
392 1
        if item.SourceNodeId not in self._aspace:
393
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
394 1
        if item.TargetNodeId not in self._aspace:
395
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
396 1
        if item.ReferenceTypeId not in self._aspace:
397
            return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
398 1
        if user != User.Admin:
399
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
400
401 1
        if item.DeleteBidirectional:
402 1
            self._delete_unique_reference(item, True)
403 1
        return self._delete_unique_reference(item)
404
405 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
406 1
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
407 1
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
408 1
            dv.ServerTimestamp = datetime.utcnow()
409 1
            dv.SourceTimestamp = datetime.utcnow()
410 1
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
411
412 1
    def _add_nodeattributes(self, item, nodedata):
413 1
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
414 1
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
415 1
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
416 1
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
417 1
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
418 1
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
419 1
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
420 1
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
421 1
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
422 1
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
423 1
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
424 1
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
425 1
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
426 1
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
427 1
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
428 1
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
429 1
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
430 1
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
431 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
432 1
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
433 1
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
434 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
435 1
        self._add_node_attr(item, nodedata, "Value")
436
437
438 1
class MethodService(object):
439
440 1
    def __init__(self, aspace):
441 1
        self.logger = logging.getLogger(__name__)
442 1
        self._aspace = aspace
443
444 1
    def call(self, methods):
445 1
        results = []
446 1
        for method in methods:
447 1
            results.append(self._call(method))
448 1
        return results
449
450 1
    def _call(self, method):
451 1
        res = ua.CallMethodResult()
452 1
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
453 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
454
        else:
455 1
            node = self._aspace[method.MethodId]
456 1
            if node.call is None:
457
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
458
            else:
459 1
                try:
460 1
                    result = node.call(method.ObjectId, *method.InputArguments)
461 1
                    if isinstance(result, ua.CallMethodResult):
462 1
                        res = result
463 1
                    elif isinstance(result, ua.StatusCode):
464 1
                        res.StatusCode = result
465
                    else:
466 1
                        res.OutputArguments = result
467 1
                    while len(res.InputArgumentResults) < len(method.InputArguments):
468 1
                        res.InputArgumentResults.append(ua.StatusCode())
469 1
                except Exception:
470 1
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
471 1
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
472 1
        return res
473
474
475 1
class AddressSpace(object):
476
477
    """
478
    The address space object stores all the nodes of the OPC-UA server
479
    and helper methods.
480
    The methods are thread safe
481
    """
482
483 1
    def __init__(self):
484 1
        self.logger = logging.getLogger(__name__)
485 1
        self._nodes = {}
486 1
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
487 1
        self._datachange_callback_counter = 200
488 1
        self._handle_to_attribute_map = {}
489 1
        self._default_idx = 2
490 1
        self._nodeid_counter = {0: 20000, 1: 2000}
491
492 1
    def __getitem__(self, nodeid):
493 1
        with self._lock:
494 1
            return self._nodes.__getitem__(nodeid)
495
496 1
    def get(self, nodeid):
497 1
        with self._lock:
498 1
            return self._nodes.get(nodeid, None)
499
500 1
    def __setitem__(self, nodeid, value):
501 1
        with self._lock:
502 1
            return self._nodes.__setitem__(nodeid, value)
503
504 1
    def __contains__(self, nodeid):
505 1
        with self._lock:
506 1
            return self._nodes.__contains__(nodeid)
507
508 1
    def __delitem__(self, nodeid):
509 1
        with self._lock:
510 1
            self._nodes.__delitem__(nodeid)
511
512 1
    def generate_nodeid(self, idx=None):
513 1
        if idx is None:
514
            idx = self._default_idx
515 1
        if idx in self._nodeid_counter:
516 1
            self._nodeid_counter[idx] += 1
517
        else:
518 1
            self._nodeid_counter[idx] = 1
519 1
        nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
520 1
        with self._lock:  # OK since reentrant lock
521 1
            while True:
522 1
                if nodeid in self._nodes:
523 1
                    nodeid = self.generate_nodeid(idx)
524
                else:
525 1
                    return nodeid
526
527 1
    def keys(self):
528 1
        with self._lock:
529 1
            return self._nodes.keys()
530
531 1
    def empty(self):
532
        """
533
        Delete all nodes in address space
534
        """
535
        with self._lock:
536
            self._nodes = {}
537
538 1
    def dump(self, path):
539
        """
540
        Dump address space as binary to file; note that server must be stopped for this method to work
541
        DO NOT DUMP AN ADDRESS SPACE WHICH IS USING A SHELF (load_aspace_shelf), ONLY CACHED NODES WILL GET DUMPED!
542
        """
543
        # prepare nodes in address space for being serialized
544
        for nodeid, ndata in self._nodes.items():
545
            # if the node has a reference to a method call, remove it so the object can be serialized
546
            if ndata.call is not None:
547
                self._nodes[nodeid].call = None
548
549
        with open(path, 'wb') as f:
550
            pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
551
552 1
    def load(self, path):
553
        """
554
        Load address space from a binary file, overwriting everything in the current address space
555
        """
556
        with open(path, 'rb') as f:
557
            self._nodes = pickle.load(f)
558
559 1
    def make_aspace_shelf(self, path):
560
        """
561
        Make a shelf for containing the nodes from the standard address space; this is typically only done on first
562
        start of the server. Subsequent server starts will load the shelf, nodes are then moved to a cache
563
        by the LazyLoadingDict class when they are accessed. Saving data back to the shelf
564
        is currently NOT supported, it is only used for the default OPC UA standard address space
565
566
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
567
        """
568
        s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
569
        for nodeid, ndata in self._nodes.items():
570
            s[nodeid.to_string()] = ndata
571
        s.close()
572
573 1
    def load_aspace_shelf(self, path):
574
        """
575
        Load the standard address space nodes from a python shelve via LazyLoadingDict as needed.
576
        The dump() method can no longer be used if the address space is being loaded from a shelf
577
578
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
579
        """
580
        class LazyLoadingDict(collections.MutableMapping):
581
            """
582
            Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied from the
583
            shelve to the cache dict. All user nodes are saved in the cache ONLY. Saving data back to the shelf
584
            is currently NOT supported
585
            """
586
            def __init__(self, source):
587
                self.source = source  # python shelf
588
                self.cache = {}  # internal dict
589
590
            def __getitem__(self, key):
591
                # try to get the item (node) from the cache, if it isn't there get it from the shelf
592
                try:
593
                    return self.cache[key]
594
                except KeyError:
595
                    node = self.cache[key] = self.source[key.to_string()]
596
                    return node
597
598
            def __setitem__(self, key, value):
599
                # add a new item to the cache; if this item is in the shelf it is not updated
600
                self.cache[key] = value
601
602
            def __contains__(self, key):
603
                return key in self.cache or key.to_string() in self.source
604
605
            def __delitem__(self, key):
606
                # only deleting items from the cache is allowed
607
                del self.cache[key]
608
609
            def __iter__(self):
610
                # only the cache can be iterated over
611
                return iter(self.cache.keys())
612
613
            def __len__(self):
614
                # only returns the length of items in the cache, not unaccessed items in the shelf
615
                return len(self.cache)
616
617
        self._nodes = LazyLoadingDict(shelve.open(path, "r"))
618
619 1
    def get_attribute_value(self, nodeid, attr):
620 1
        with self._lock:
621 1
            self.logger.debug("get attr val: %s %s", nodeid, attr)
622 1
            if nodeid not in self._nodes:
623 1
                dv = ua.DataValue()
624 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
625 1
                return dv
626 1
            node = self._nodes[nodeid]
627 1
            if attr not in node.attributes:
628 1
                dv = ua.DataValue()
629 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
630 1
                return dv
631 1
            attval = node.attributes[attr]
632 1
            if attval.value_callback:
633
                return attval.value_callback()
634 1
            return attval.value
635
636 1
    def set_attribute_value(self, nodeid, attr, value):
637 1
        with self._lock:
638 1
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
639 1
            if nodeid not in self._nodes:
640 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
641 1
            node = self._nodes[nodeid]
642 1
            if attr not in node.attributes:
643 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
644 1
            if not value.SourceTimestamp:
645 1
                value.SourceTimestamp = datetime.utcnow()
646 1
            if not value.ServerTimestamp:
647 1
                value.ServerTimestamp = datetime.utcnow()
648
649 1
            attval = node.attributes[attr]
650 1
            old = attval.value
651 1
            attval.value = value
652 1
            cbs = []
653 1
            if old.Value != value.Value:  # only send call callback when a value change has happend
654 1
                cbs = list(attval.datachange_callbacks.items())
655
656 1
        for k, v in cbs:
657 1
            try:
658 1
                v(k, value)
659
            except Exception as ex:
660
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
661
662 1
        return ua.StatusCode()
663
664 1
    def add_datachange_callback(self, nodeid, attr, callback):
665 1
        with self._lock:
666 1
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
667 1
            if nodeid not in self._nodes:
668
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
669 1
            node = self._nodes[nodeid]
670 1
            if attr not in node.attributes:
671 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
672 1
            attval = node.attributes[attr]
673 1
            self._datachange_callback_counter += 1
674 1
            handle = self._datachange_callback_counter
675 1
            attval.datachange_callbacks[handle] = callback
676 1
            self._handle_to_attribute_map[handle] = (nodeid, attr)
677 1
            return ua.StatusCode(), handle
678
679 1
    def delete_datachange_callback(self, handle):
680 1
        with self._lock:
681 1
            if handle in self._handle_to_attribute_map:
682 1
                nodeid, attr = self._handle_to_attribute_map.pop(handle)
683 1
                self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
684
685 1
    def add_method_callback(self, methodid, callback):
686 1
        with self._lock:
687 1
            node = self._nodes[methodid]
688
            node.call = callback
689