Completed
Pull Request — master (#509)
by
unknown
07:05
created

NodeManagementService._delete_unique_reference()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 12
ccs 0
cts 11
cp 0
crap 20
rs 9.2
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
except:
9
    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 _add_node(self, item, user):
192 1
        result = ua.AddNodesResult()
193
194
        # If Identifier of requested NodeId is null we generate a new NodeId using
195
        # the namespace of the nodeid, this is an extention of the spec to allow
196
        # to requests the server to generate a new nodeid in a specified namespace
197 1
        if item.RequestedNewNodeId.has_null_identifier():
198 1
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
199 1
            nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
200
        else:
201 1
            nodedata = NodeData(item.RequestedNewNodeId)
202
203 1
        if nodedata.nodeid in self._aspace:
204 1
            self.logger.warning("AddNodesItem: Requested NodeId %s already exists", nodedata.nodeid)
205 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
206 1
            return result
207
208 1
        if item.ParentNodeId.is_null():
209
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
210
            # should return Error here, but the standard namespace define many nodes without parents...
211 1
            pass
212 1
        elif item.ParentNodeId not in self._aspace:
213 1
            self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
214 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
215 1
            return result
216
217 1
        if not user == User.Admin:
218 1
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
219 1
            return result
220
221 1
        self._add_node_attributes(nodedata, item)
222
223
        # now add our node to db
224 1
        self._aspace[nodedata.nodeid] = nodedata
225
226 1
        if not item.ParentNodeId.is_null():
227 1
            self._add_ref_from_parent(nodedata, item)
228 1
            self._add_ref_to_parent(nodedata, item, user)
229
230
        # add type definition
231 1
        if item.TypeDefinition != ua.NodeId():
232 1
            self._add_type_definition(nodedata, item, user)
233
234 1
        result.StatusCode = ua.StatusCode()
235 1
        result.AddedNodeId = nodedata.nodeid
236
237 1
        return result
238
239 1
    def _add_node_attributes(self, nodedata, item):
240
        # add common attrs
241 1
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
242
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
243
        )
244 1
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
245
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
246
        )
247 1
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
248
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
249
        )
250
        # add requested attrs
251 1
        self._add_nodeattributes(item.NodeAttributes, nodedata)
252
    @staticmethod
253 1
    def _filter_matching_references(refs, nodeid, reftype):
254 1
        return filter(lambda r: r.NodeId == nodeid and r.ReferenceTypeId == reftype, refs)
255 1
256 1
    def _add_unique_reference(self, source, desc):
257 1
        refs = self._aspace[source].references
258 1
        for r in self._filter_matching_references(refs, desc.NodeId, desc.ReferenceTypeId):
259 1
            if r.IsForward !=  desc.IsForward:
260 1
                self.logger.error("Cannot add conflicting reference %s ", str(desc))
261 1
                return ua.StatusCode(ua.StatusCodes.BadReferenceNotAllowed)
262 1
            break # ref already exists
263
        else:
264 1
            refs.append(desc)
265 1
        return ua.StatusCode()
266 1
267 1
    def _add_ref_from_parent(self, nodedata, item):
268 1
        desc = ua.ReferenceDescription()
269 1
        desc.ReferenceTypeId = item.ReferenceTypeId
270 1
        desc.NodeId = nodedata.nodeid
271 1
        desc.NodeClass = item.NodeClass
272
        desc.BrowseName = item.BrowseName
273 1
        desc.DisplayName = item.NodeAttributes.DisplayName
274 1
        desc.TypeDefinition = item.TypeDefinition
275 1
        desc.IsForward = True
276 1
        self._add_unique_reference(item.ParentNodeId, desc)
277 1
278 1
    def _add_ref_to_parent(self, nodedata, item, user):
279 1
        addref = ua.AddReferencesItem()
280 1
        addref.ReferenceTypeId = item.ReferenceTypeId
281
        addref.SourceNodeId = nodedata.nodeid
282 1
        addref.TargetNodeId = item.ParentNodeId
283 1
        addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
284 1
        addref.IsForward = False
285 1
        self._add_reference(addref, user)
286 1
287
    def _add_type_definition(self, nodedata, item, user):
288 1
        addref = ua.AddReferencesItem()
289 1
        addref.SourceNodeId = nodedata.nodeid
290
        addref.IsForward = True
291
        addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
292 1
        addref.TargetNodeId = item.TypeDefinition
293
        addref.TargetNodeClass = ua.NodeClass.DataType
294
        self._add_reference(addref, user)
295
296 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
297 1
        return map(lambda item: self._delete_node(item, user), deletenodeitems.NodesToDelete)
298 1
299 1
    def _delete_node(self, item, user):
300 1
        if user != User.Admin:
301
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
302 1
303
        if item.NodeId not in self._aspace:
304 1
            self.logger.warning("DeleteNodesItem: NodeId %s does not exists", item.NodeId)
305
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
306 1
307
        if item.DeleteTargetReferences:
308 1
            for elem in self._aspace.keys():
309 1
                for rdesc in self._aspace[elem].references:
310 1
                    if rdesc.NodeId == item.NodeId:
311
                        self._aspace[elem].references.remove(rdesc)
312
313
        self._delete_node_callbacks(self._aspace[item.NodeId])
314
315
        del(self._aspace[item.NodeId])
316
317 1
        return ua.StatusCode()
318 1
319 1
    def _delete_node_callbacks(self, nodedata):
320 1
        if ua.AttributeIds.Value in nodedata.attributes:
321 1
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
322
                try:
323 1
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
324 1
                    self._aspace.delete_datachange_callback(handle)
325
                except Exception as ex:
326 1
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
327 1
328 1
    def add_references(self, refs, user=User.Admin):
329
        return map(lambda ref: self._add_reference(ref, user), refs)
330 1
331 1
    def try_add_references(self, refs, user=User.Admin):
332 1
        for ref in refs:
333 1
            if not self._add_reference(ref, user).is_good():
334 1
                yield ref
335 1
336 1
    def _add_reference(self, addref, user):
337 1
        if addref.SourceNodeId not in self._aspace:
338 1
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
339 1
        if addref.TargetNodeId not in self._aspace:
340 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
341 1
        if user != User.Admin:
342 1
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
343
        rdesc = ua.ReferenceDescription()
344 1
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
345
        rdesc.IsForward = addref.IsForward
346
        rdesc.NodeId = addref.TargetNodeId
347
        rdesc.NodeClass = addref.TargetNodeClass
348
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
349
        if bname:
350 1
            rdesc.BrowseName = bname
351
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
352
        if dname:
353
            rdesc.DisplayName = dname
354
        return self._add_unique_reference(addref.SourceNodeId, rdesc)
355
356
    def delete_references(self, refs, user=User.Admin):
357
        return map(lambda ref: self._delete_reference(ref, user), refs)
358
359
    def _delete_unique_reference(self, item, invert = False):
360
        if invert:
361
            source, target, forward = item.TargetNodeId, item.SourceNodeId, not item.IsForward
362
        else:
363
            source, target, forward = item.SourceNodeId, item.TargetNodeId, item.IsForward
364
365
        for rdesc in self._filter_matching_references(self._aspace[source].references, target, item.ReferenceTypeId):
366
            if rdesc.IsForward == forward:
367
                self._aspace[source].references.remove(rdesc)
368
                return ua.StatusCode()
369
	    break
370
        return ua.StatusCode(ua.StatusCodes.BadNotFound)
371
372
    def _delete_reference(self, item, user):
373
        if item.SourceNodeId not in self._aspace:
374 1
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
375 1
        if item.TargetNodeId not in self._aspace:
376 1
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
377 1
        if item.ReferenceTypeId not in self._aspace:
378 1
            return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
379 1
        if user != User.Admin:
380
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
381 1
382 1
        if item.DeleteBidirectional:
383 1
            self._delete_unique_reference(item, True)
384 1
        return self._delete_unique_reference(item)
385 1
386 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
387 1
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
388 1
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
389 1
            dv.ServerTimestamp = datetime.utcnow()
390 1
            dv.SourceTimestamp = datetime.utcnow()
391 1
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
392 1
393 1
    def _add_nodeattributes(self, item, nodedata):
394 1
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
395 1
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
396 1
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
397 1
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
398 1
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
399 1
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
400 1
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
401 1
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
402 1
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
403 1
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
404 1
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
405
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
406
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
407 1
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
408
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
409 1
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
410 1
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
411 1
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
412
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
413 1
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
414 1
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
415 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
416 1
        self._add_node_attr(item, nodedata, "Value")
417 1
418
419 1
class MethodService(object):
420 1
421 1
    def __init__(self, aspace):
422 1
        self.logger = logging.getLogger(__name__)
423
        self._aspace = aspace
424 1
425 1
    def call(self, methods):
426
        results = []
427
        for method in methods:
428 1
            results.append(self._call(method))
429 1
        return results
430 1
431 1
    def _call(self, method):
432 1
        res = ua.CallMethodResult()
433 1
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
434 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
435 1
        else:
436
            node = self._aspace[method.MethodId]
437
            if node.call is None:
438 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
439
            else:
440
                try:
441
                    res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
442
                    for _ in method.InputArguments:
443
                        res.InputArgumentResults.append(ua.StatusCode())
444
                except Exception:
445
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
446 1
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
447 1
        return res
448 1
449 1
450 1
class AddressSpace(object):
451 1
452 1
    """
453 1
    The address space object stores all the nodes of the OPC-UA server
454
    and helper methods.
455 1
    The methods are thread safe
456 1
    """
457 1
458 1
    def __init__(self):
459
        self.logger = logging.getLogger(__name__)
460 1
        self._nodes = {}
461 1
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
462 1
        self._datachange_callback_counter = 200
463
        self._handle_to_attribute_map = {}
464 1
        self._default_idx = 2
465 1
        self._nodeid_counter = {0: 20000, 1: 2000}
466 1
467
    def __getitem__(self, nodeid):
468 1
        with self._lock:
469 1
            if nodeid in self._nodes:
470 1
                return self._nodes.__getitem__(nodeid)
471
472 1
    def __setitem__(self, nodeid, value):
473 1
        with self._lock:
474
            return self._nodes.__setitem__(nodeid, value)
475 1
476 1
    def __contains__(self, nodeid):
477
        with self._lock:
478 1
            return self._nodes.__contains__(nodeid)
479 1
480 1
    def __delitem__(self, nodeid):
481 1
        with self._lock:
482 1
            self._nodes.__delitem__(nodeid)
483 1
484
    def generate_nodeid(self, idx=None):
485 1
        if idx is None:
486
            idx = self._default_idx
487 1
        if idx in self._nodeid_counter:
488 1
            self._nodeid_counter[idx] += 1
489 1
        else:
490
            self._nodeid_counter[idx] = 1
491 1
        nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
492
        with self._lock:  # OK since reentrant lock
493
            while True:
494
                if nodeid in self._nodes:
495
                    nodeid = self.generate_nodeid(idx)
496
                else:
497
                    return nodeid
498 1
499
    def keys(self):
500
        with self._lock:
501
            return self._nodes.keys()
502
503
    def empty(self):
504
        """
505
        Delete all nodes in address space
506
        """
507
        with self._lock:
508
            self._nodes = {}
509
510
    def dump(self, path):
511
        """
512 1
        Dump address space as binary to file; note that server must be stopped for this method to work
513
        DO NOT DUMP AN ADDRESS SPACE WHICH IS USING A SHELF (load_aspace_shelf), ONLY CACHED NODES WILL GET DUMPED!
514
        """
515
        # prepare nodes in address space for being serialized
516
        for nodeid, ndata in self._nodes.items():
517
            # if the node has a reference to a method call, remove it so the object can be serialized
518
            if ndata.call is not None:
519 1
                self._nodes[nodeid].call = None
520
521
        with open(path, 'wb') as f:
522
            pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
523
524
    def load(self, path):
525
        """
526
        Load address space from a binary file, overwriting everything in the current address space
527
        """
528
        with open(path, 'rb') as f:
529
            self._nodes = pickle.load(f)
530
531
    def make_aspace_shelf(self, path):
532
        """
533 1
        Make a shelf for containing the nodes from the standard address space; this is typically only done on first
534
        start of the server. Subsequent server starts will load the shelf, nodes are then moved to a cache
535
        by the LazyLoadingDict class when they are accessed. Saving data back to the shelf
536
        is currently NOT supported, it is only used for the default OPC UA standard address space
537
538
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
539
        """
540
        s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
541
        for nodeid, ndata in self._nodes.items():
542
            s[nodeid.to_string()] = ndata
543
        s.close()
544
545
    def load_aspace_shelf(self, path):
546
        """
547
        Load the standard address space nodes from a python shelve via LazyLoadingDict as needed.
548
        The dump() method can no longer be used if the address space is being loaded from a shelf
549
550
        Note: Intended for slow devices, such as Raspberry Pi, to greatly improve start up time
551
        """
552
        class LazyLoadingDict(collections.MutableMapping):
553
            """
554
            Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied from the
555
            shelve to the cache dict. All user nodes are saved in the cache ONLY. Saving data back to the shelf
556
            is currently NOT supported
557
            """
558
            def __init__(self, source):
559
                self.source = source  # python shelf
560
                self.cache = {}  # internal dict
561
562
            def __getitem__(self, key):
563
                # try to get the item (node) from the cache, if it isn't there get it from the shelf
564
                try:
565
                    return self.cache[key]
566
                except KeyError:
567
                    node = self.cache[key] = self.source[key.to_string()]
568
                    return node
569
570
            def __setitem__(self, key, value):
571
                # add a new item to the cache; if this item is in the shelf it is not updated
572
                self.cache[key] = value
573
574
            def __contains__(self, key):
575
                return key in self.cache or key.to_string() in self.source
576
577
            def __delitem__(self, key):
578
                # only deleting items from the cache is allowed
579 1
                del self.cache[key]
580 1
581 1
            def __iter__(self):
582 1
                # only the cache can be iterated over
583 1
                return iter(self.cache.keys())
584 1
585 1
            def __len__(self):
586 1
                # only returns the length of items in the cache, not unaccessed items in the shelf
587 1
                return len(self.cache)
588 1
589 1
        self._nodes = LazyLoadingDict(shelve.open(path, "r"))
590 1
591 1
    def get_attribute_value(self, nodeid, attr):
592 1
        with self._lock:
593
            self.logger.debug("get attr val: %s %s", nodeid, attr)
594 1
            if nodeid not in self._nodes:
595
                dv = ua.DataValue()
596 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
597 1
                return dv
598 1
            node = self._nodes[nodeid]
599 1
            if attr not in node.attributes:
600 1
                dv = ua.DataValue()
601 1
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
602 1
                return dv
603 1
            attval = node.attributes[attr]
604 1
            if attval.value_callback:
605 1
                return attval.value_callback()
606 1
            return attval.value
607 1
608
    def set_attribute_value(self, nodeid, attr, value):
609 1
        with self._lock:
610 1
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
611 1
            if nodeid not in self._nodes:
612 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
613 1
            node = self._nodes[nodeid]
614 1
            if attr not in node.attributes:
615
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
616 1
            if not value.SourceTimestamp:
617 1
                value.SourceTimestamp = datetime.utcnow()
618 1
            if not value.ServerTimestamp:
619
                value.ServerTimestamp = datetime.utcnow()
620
621
            attval = node.attributes[attr]
622 1
            old = attval.value
623
            attval.value = value
624 1
            cbs = []
625 1
            if old.Value != value.Value:  # only send call callback when a value change has happend
626 1
                cbs = list(attval.datachange_callbacks.items())
627 1
628
        for k, v in cbs:
629 1
            try:
630 1
                v(k, value)
631 1
            except Exception as ex:
632 1
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
633 1
634 1
        return ua.StatusCode()
635 1
636 1
    def add_datachange_callback(self, nodeid, attr, callback):
637 1
        with self._lock:
638
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
639 1
            if nodeid not in self._nodes:
640 1
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
641 1
            node = self._nodes[nodeid]
642 1
            if attr not in node.attributes:
643 1
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
644
            attval = node.attributes[attr]
645 1
            self._datachange_callback_counter += 1
646 1
            handle = self._datachange_callback_counter
647 1
            attval.datachange_callbacks[handle] = callback
648 1
            self._handle_to_attribute_map[handle] = (nodeid, attr)
649
            return ua.StatusCode(), handle
650
651
    def delete_datachange_callback(self, handle):
652
        with self._lock:
653
            if handle in self._handle_to_attribute_map:
654
                nodeid, attr = self._handle_to_attribute_map.pop(handle)
655
                self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
656
657
    def add_method_callback(self, methodid, callback):
658
        with self._lock:
659
            node = self._nodes[methodid]
660
            node.call = callback
661