Test Failed
Pull Request — master (#515)
by
unknown
05:31
created

MethodService.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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