Completed
Push — master ( a80a7b...be43b4 )
by Olivier
02:17
created

opcua.client_security()   B

Complexity

Conditions 7

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 18
rs 7.3333
cc 7
1
import logging
2
import sys
3
import argparse
4
from datetime import datetime
5
from enum import Enum
6
import math
7
import time
8
9
try:
10
    from IPython import embed
11
except ImportError:
12
    import code
13
14
    def embed():
15
        code.interact(local=dict(globals(), **locals())) 
16
17
from opcua import ua
18
from opcua import Client
19
from opcua import Server
20
from opcua import Node
21
from opcua import uamethod
22
from opcua import security_policies
23
24
25
def add_minimum_args(parser):
26
    parser.add_argument("-u",
27
                        "--url",
28
                        help="URL of OPC UA server (for example: opc.tcp://example.org:4840)",
29
                        default='opc.tcp://localhost:4841',
30
                        metavar="URL")
31
    parser.add_argument("-v",
32
                        "--verbose",
33
                        dest="loglevel",
34
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
35
                        default='WARNING',
36
                        help="Set log level")
37
    parser.add_argument("--timeout",
38
                        dest="timeout",
39
                        type=int,
40
                        default=1,
41
                        help="Set socket timeout (NOT the diverse UA timeouts)")
42
43
44
def add_common_args(parser):
45
    add_minimum_args(parser)
46
    parser.add_argument("-n",
47
                        "--nodeid",
48
                        help="Fully-qualified node ID (for example: i=85). Default: root node",
49
                        default='i=84',
50
                        metavar="NODE")
51
    parser.add_argument("-p",
52
                        "--path",
53
                        help="Comma separated browse path to the node starting at NODE (for example: 3:Mybject,3:MyVariable)",
54
                        default='',
55
                        metavar="BROWSEPATH")
56
    parser.add_argument("-i",
57
                        "--namespace",
58
                        help="Default namespace",
59
                        type=int,
60
                        default=0,
61
                        metavar="NAMESPACE")
62
    parser.add_argument("--security",
63
                        help="Security settings, for example: Basic256,SignAndEncrypt,cert.der,pk.pem[,server_cert.der]. Default: None",
64
                        default='')
65
66
67
def client_security(security, url, timeout):
68
    parts = security.split(',')
69
    if len(parts) < 4:
70
        raise Exception('Wrong format: `{}`, expected at least 4 comma-separated values'.format(security))
71
    policy_class = getattr(security_policies, 'SecurityPolicy' + parts[0])
72
    mode = getattr(ua.MessageSecurityMode, parts[1])
73
    cert = open(parts[2], 'rb').read()
74
    pk = open(parts[3], 'rb').read()
75
    server_cert = None
76
    if len(parts) == 5:
77
        server_cert = open(parts[4], 'rb').read()
78
    else:
79
        # we need server's certificate too. Let's get it from the list of endpoints
80
        client = Client(url, timeout=timeout)
81
        for ep in client.connect_and_get_server_endpoints():
82
            if ep.EndpointUrl.startswith(ua.OPC_TCP_SCHEME) and ep.SecurityMode == mode and ep.SecurityPolicyUri == policy_class.URI:
83
                server_cert = ep.ServerCertificate
84
    return policy_class(server_cert, cert, pk, mode)
85
86
def parse_args(parser, requirenodeid=False):
87
    args = parser.parse_args()
88
    logging.basicConfig(format="%(levelname)s: %(message)s", level=getattr(logging, args.loglevel))
89
    if args.url and '://' not in args.url:
90
        logging.info("Adding default scheme %s to URL %s", ua.OPC_TCP_SCHEME, args.url)
91
        args.url = ua.OPC_TCP_SCHEME + '://' + args.url
92
    if hasattr(args, 'security') and args.security:
93
        args.security = client_security(args.security, args.url, args.timeout)
94
    # check that a nodeid has been given explicitly, a bit hackish...
95
    if requirenodeid and args.nodeid == "i=84" and args.path == "":
96
        parser.print_usage()
97
        print("{}: error: A NodeId or BrowsePath is required".format(parser.prog))
98
        sys.exit(1)
99
    return args
100
101
102
def get_node(client, args):
103
    node = client.get_node(args.nodeid)
104
    if args.path:
105
        node = node.get_child(args.path.split(","))
106
    return node
107
108
109
def uaread():
110
    parser = argparse.ArgumentParser(description="Read attribute of a node, per default reads value of a node")
111
    add_common_args(parser)
112
    parser.add_argument("-a",
113
                        "--attribute",
114
                        dest="attribute",
115
                        type=int,
116
                        default=ua.AttributeIds.Value,
117
                        help="Set attribute to read")
118
    parser.add_argument("-t",
119
                        "--datatype",
120
                        dest="datatype",
121
                        default="python",
122
                        choices=['python', 'variant', 'datavalue'],
123
                        help="Data type to return")
124
125
    args = parse_args(parser, requirenodeid=True)
126
127
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
128
    client.connect()
129
    try:
130
        node = get_node(client, args)
131
        attr = node.get_attribute(args.attribute)
132
        if args.datatype == "python":
133
            print(attr.Value.Value)
134
        elif args.datatype == "variant":
135
            print(attr.Value)
136
        else:
137
            print(attr)
138
    finally:
139
        client.disconnect()
140
    sys.exit(0)
141
    print(args)
142
143
144
def _args_to_array(val, array):
145
    if array == "guess":
146
        if "," in val:
147
            array = "true"
148
    if array == "true":
149
        val = val.split(",")
150
    return val
151
152
153
def _arg_to_bool(val):
154
    if val in ("true", "True"):
155
        return True
156
    else:
157
        return False
158
159
160
def _arg_to_variant(val, array, ptype, varianttype=None):
161
    val = _args_to_array(val, array)
162
    if isinstance(val, list):
163
        val = [ptype(i) for i in val]
164
    else:
165
        val = ptype(val)
166
    if varianttype:
167
        return ua.Variant(val, varianttype)
168
    else:
169
        return ua.Variant(val)
170
171
172
def _val_to_variant(val, args):
173
    array = args.array
174
    if args.datatype == "guess":
175
        if val in ("true", "True", "false", "False"):
176
            return _arg_to_variant(val, array, _arg_to_bool)
177
        # FIXME: guess bool value
178
        try:
179
            return _arg_to_variant(val, array, int)
180
        except ValueError:
181
            try:
182
                return _arg_to_variant(val, array, float)
183
            except ValueError:
184
                return _arg_to_variant(val, array, str)
185
    elif args.datatype == "bool":
186
        if val in ("1", "True", "true"):
187
            return ua.Variant(True, ua.VariantType.Boolean)
188
        else:
189
            return ua.Variant(False, ua.VariantType.Boolean)
190
    elif args.datatype == "sbyte":
191
        return _arg_to_variant(val, array, int, ua.VariantType.SByte)
192
    elif args.datatype == "byte":
193
        return _arg_to_variant(val, array, int, ua.VariantType.Byte)
194
    #elif args.datatype == "uint8":
195
        #return _arg_to_variant(val, array, int, ua.VariantType.Byte)
196
    elif args.datatype == "uint16":
197
        return _arg_to_variant(val, array, int, ua.VariantType.UInt16)
198
    elif args.datatype == "uint32":
199
        return _arg_to_variant(val, array, int, ua.VariantType.UInt32)
200
    elif args.datatype == "uint64":
201
        return _arg_to_variant(val, array, int, ua.VariantType.UInt64)
202
    #elif args.datatype == "int8":
203
        #return ua.Variant(int(val), ua.VariantType.Int8)
204
    elif args.datatype == "int16":
205
        return _arg_to_variant(val, array, int, ua.VariantType.Int16)
206
    elif args.datatype == "int32":
207
        return _arg_to_variant(val, array, int, ua.VariantType.Int32)
208
    elif args.datatype == "int64":
209
        return _arg_to_variant(val, array, int, ua.VariantType.Int64)
210
    elif args.datatype == "float":
211
        return _arg_to_variant(val, array, float, ua.VariantType.Float)
212
    elif args.datatype == "double":
213
        return _arg_to_variant(val, array, float, ua.VariantType.Double)
214
    elif args.datatype == "string":
215
        return _arg_to_variant(val, array, str, ua.VariantType.String)
216
    elif args.datatype == "datetime":
217
        raise NotImplementedError
218
    elif args.datatype == "Guid":
219
        return _arg_to_variant(val, array, bytes, ua.VariantType.Guid)
220
    elif args.datatype == "ByteString":
221
        return _arg_to_variant(val, array, bytes, ua.VariantType.ByteString)
222
    elif args.datatype == "xml":
223
        return _arg_to_variant(val, array, str, ua.VariantType.XmlElement)
224
    elif args.datatype == "nodeid":
225
        return _arg_to_variant(val, array, ua.NodeId.from_string, ua.VariantType.NodeId)
226
    elif args.datatype == "expandednodeid":
227
        return _arg_to_variant(val, array, ua.ExpandedNodeId.from_string, ua.VariantType.ExpandedNodeId)
228
    elif args.datatype == "statuscode":
229
        return _arg_to_variant(val, array, int, ua.VariantType.StatusCode)
230
    elif args.datatype in ("qualifiedname", "browsename"):
231
        return _arg_to_variant(val, array, ua.QualifiedName.from_string, ua.VariantType.QualifiedName)
232
    elif args.datatype == "LocalizedText":
233
        return _arg_to_variant(val, array, ua.LocalizedText, ua.VariantType.LocalizedText)
234
235
236
def uawrite():
237
    parser = argparse.ArgumentParser(description="Write attribute of a node, per default write value of node")
238
    add_common_args(parser)
239
    parser.add_argument("-a",
240
                        "--attribute",
241
                        dest="attribute",
242
                        type=int,
243
                        default=ua.AttributeIds.Value,
244
                        help="Set attribute to read")
245
    parser.add_argument("-l",
246
                        "--list",
247
                        "--array",
248
                        dest="array",
249
                        default="guess",
250
                        choices=["guess", "true", "false"],
251
                        help="Value is an array")
252
    parser.add_argument("-t",
253
                        "--datatype",
254
                        dest="datatype",
255
                        default="guess",
256
                        choices=["guess", 'byte', 'sbyte', 'nodeid', 'expandednodeid', 'qualifiedname', 'browsename', 'string', 'float', 'double', 'int16', 'int32', "int64", 'uint16', 'uint32', 'uint64', "bool", "string", 'datetime', 'bytestring', 'xmlelement', 'statuscode', 'localizedtext'],  
257
                        help="Data type to return")
258
    parser.add_argument("value",
259
                        help="Value to be written",
260
                        metavar="VALUE")
261
    args = parse_args(parser, requirenodeid=True)
262
263
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
264
    client.connect()
265
    try:
266
        node = get_node(client, args)
267
        val = _val_to_variant(args.value, args)
268
        node.set_attribute(args.attribute, ua.DataValue(val))
269
    finally:
270
        client.disconnect()
271
    sys.exit(0)
272
    print(args)
273
274
275
def uals():
276
    parser = argparse.ArgumentParser(description="Browse OPC-UA node and print result")
277
    add_common_args(parser)
278
    parser.add_argument("-l",
279
                        dest="long_format",
280
                        const=3,
281
                        nargs="?",
282
                        type=int,
283
                        help="use a long listing format")
284
    parser.add_argument("-d",
285
                        "--depth",
286
                        default=1,
287
                        type=int,
288
                        help="Browse depth")
289
290
    args = parse_args(parser)
291
    if args.long_format is None:
292
        args.long_format = 1
293
294
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
295
    client.connect()
296
    try:
297
        node = get_node(client, args)
298
        print("Browsing node {} at {}\n".format(node, args.url))
299
        if args.long_format == 0:
300
            _lsprint_0(node, args.depth - 1)
301
        elif args.long_format == 1:
302
            _lsprint_1(node, args.depth - 1)
303
        else:
304
            _lsprint_long(node, args.depth - 1)
305
    finally:
306
        client.disconnect()
307
    sys.exit(0)
308
    print(args)
309
310
311
def _lsprint_0(node, depth, indent=""):
312
    if not indent:
313
        print("{:30} {:25}".format("DisplayName", "NodeId"))
314
        print("")
315
    for desc in node.get_children_descriptions():
316
        print("{}{:30} {:25}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string()))
317
        if depth:
318
            _lsprint_0(Node(node.server, desc.NodeId), depth - 1, indent + "  ")
319
320
321
def _lsprint_1(node, depth, indent=""):
322
    if not indent:
323
        print("{:30} {:25} {:25} {:25}".format("DisplayName", "NodeId", "BrowseName", "Value"))
324
        print("")
325
326
    for desc in node.get_children_descriptions():
327
        if desc.NodeClass == ua.NodeClass.Variable:
328
            val = Node(node.server, desc.NodeId).get_value()
329
            print("{}{:30} {!s:25} {!s:25}, {!s:3}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string(), desc.BrowseName.to_string(), val))
330
        else:
331
            print("{}{:30} {!s:25} {!s:25}".format(indent, desc.DisplayName.to_string(), desc.NodeId.to_string(), desc.BrowseName.to_string()))
332
        if depth:
333
            _lsprint_1(Node(node.server, desc.NodeId), depth - 1, indent + "  ")
334
335
336
def _lsprint_long(pnode, depth, indent=""):
337
    if not indent:
338
        print("{:30} {:25} {:25} {:10} {:30} {:25}".format("DisplayName", "NodeId", "BrowseName", "DataType", "Timestamp", "Value"))
339
        print("")
340
    for node in pnode.get_children():
341
        attrs = node.get_attributes([ua.AttributeIds.DisplayName, 
342
                                     ua.AttributeIds.BrowseName,
343
                                     ua.AttributeIds.NodeClass,
344
                                     ua.AttributeIds.WriteMask,
345
                                     ua.AttributeIds.UserWriteMask,
346
                                     ua.AttributeIds.DataType,
347
                                     ua.AttributeIds.Value])
348
        name, bname, nclass, mask, umask, dtype, val = [attr.Value.Value for attr in attrs]
349
        update = attrs[-1].ServerTimestamp
350
        if nclass == ua.NodeClass.Variable:
351
            print("{}{:30} {:25} {:25} {:10} {!s:30} {!s:25}".format(indent, name.to_string(), node.nodeid.to_string(), bname.to_string(), dtype.to_string(), update, val))
352
        else:
353
            print("{}{:30} {:25} {:25}".format(indent, name.to_string(), bname.to_string(), node.nodeid.to_string()))
354
        if depth:
355
            _lsprint_long(node, depth - 1, indent + "  ")
356
357
358
class SubHandler(object):
359
360
    def data_change(self, handle, node, val, attr):
361
        print("New data change event", handle, node, val, attr)
362
363
    def event(self, handle, event):
364
        print("New event", handle, event)
365
366
367
def uasubscribe():
368
    parser = argparse.ArgumentParser(description="Subscribe to a node and print results")
369
    add_common_args(parser)
370
    parser.add_argument("-t",
371
                        "--eventtype",
372
                        dest="eventtype",
373
                        default="datachange",
374
                        choices=['datachange', 'event'],
375
                        help="Event type to subscribe to")
376
377
    args = parse_args(parser, requirenodeid=True)
378
379
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
380
    client.connect()
381
    try:
382
        node = get_node(client, args)
383
        handler = SubHandler()
384
        sub = client.create_subscription(500, handler)
385
        if args.eventtype == "datachange":
386
            sub.subscribe_data_change(node)
387
        else:
388
            sub.subscribe_events(node)
389
        embed()
390
    finally:
391
        client.disconnect()
392
    sys.exit(0)
393
    print(args)
394
395
396
# converts numeric value to its enum name.
397
def enum_to_string(klass, value):
398
    if isinstance(value, Enum):
399
        return value.name
400
    # if value is not a subtype of Enum, try to find a constant
401
    # with this value in this class
402
    for k, v in vars(klass).items():
403
        if not k.startswith('__') and v == value:
404
            return k
405
    return 'Unknown {} ({})'.format(klass.__name__, value)
406
407
408
def application_to_strings(app):
409
    result = []
410
    result.append(('Application URI', app.ApplicationUri))
411
    optionals = [
412
        ('Product URI', app.ProductUri),
413
        ('Application Name', app.ApplicationName.to_string()),
414
        ('Application Type', enum_to_string(ua.ApplicationType, app.ApplicationType)),
415
        ('Gateway Server URI', app.GatewayServerUri),
416
        ('Discovery Profile URI', app.DiscoveryProfileUri),
417
    ]
418
    for (n, v) in optionals:
419
        if v:
420
            result.append((n, v))
421
    for url in app.DiscoveryUrls:
422
        result.append(('Discovery URL', url))
423
    return result  # ['{}: {}'.format(n, v) for (n, v) in result]
424
425
426
def endpoint_to_strings(ep):
427
    result = [('Endpoint URL', ep.EndpointUrl)]
428
    result += application_to_strings(ep.Server)
429
    result += [
430
        ('Server Certificate', len(ep.ServerCertificate)),
431
        ('Security Mode', enum_to_string(ua.MessageSecurityMode, ep.SecurityMode)),
432
        ('Security Policy URI', ep.SecurityPolicyUri)]
433
    for tok in ep.UserIdentityTokens:
434
        result += [
435
            ('User policy', tok.PolicyId),
436
            ('  Token type', enum_to_string(ua.UserTokenType, tok.TokenType))]
437
        if tok.IssuedTokenType or tok.IssuerEndpointUrl:
438
            result += [
439
                ('  Issued Token type', tok.IssuedTokenType),
440
                ('  Issuer Endpoint URL', tok.IssuerEndpointUrl)]
441
        if tok.SecurityPolicyUri:
442
            result.append(('  Security Policy URI', tok.SecurityPolicyUri))
443
    result += [
444
        ('Transport Profile URI', ep.TransportProfileUri),
445
        ('Security Level', ep.SecurityLevel)]
446
    return result
447
448
449
def uaclient():
450
    parser = argparse.ArgumentParser(description="Connect to server and start python shell. root and objects nodes are available. Node specificed in command line is available as mynode variable")
451
    add_common_args(parser)
452
    parser.add_argument("-c",
453
                        "--certificate",
454
                        help="set client certificate")
455
    parser.add_argument("-k",
456
                        "--private_key",
457
                        help="set client private key")
458
    args = parse_args(parser)
459
460
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
461
    client.connect()
462
    if args.certificate:
463
        client.load_client_certificate(args.certificate)
464
    if args.private_key:
465
        client.load_private_key(args.private_key)
466
    try:
467
        root = client.get_root_node()
468
        objects = client.get_objects_node()
469
        mynode = get_node(client, args)
470
        embed()
471
    finally:
472
        client.disconnect()
473
    sys.exit(0)
474
475
476
def uaserver():
477
    parser = argparse.ArgumentParser(description="Run an example OPC-UA server. By importing xml definition and using uawrite command line, it is even possible to expose real data using this server")
478
    # we setup a server, this is a bit different from other tool so we do not reuse common arguments
479
    parser.add_argument("-u",
480
                        "--url",
481
                        help="URL of OPC UA server, default is opc.tcp://0.0.0.0:4841",
482
                        default='opc.tcp://0.0.0.0:4841',
483
                        metavar="URL")
484
    parser.add_argument("-v",
485
                        "--verbose",
486
                        dest="loglevel",
487
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
488
                        default='WARNING',
489
                        help="Set log level")
490
    parser.add_argument("-x",
491
                        "--xml",
492
                        metavar="XML_FILE",
493
                        help="Populate address space with nodes defined in XML")
494
    parser.add_argument("-p",
495
                        "--populate",
496
                        action="store_false",
497
                        help="Populate address space with some sample nodes")
498
    parser.add_argument("-c",
499
                        "--disable-clock",
500
                        action="store_true",
501
                        help="Disable clock, to avoid seeing many write if debugging an application")
502
    parser.add_argument("-s",
503
                        "--shell",
504
                        action="store_true",
505
                        help="Start python shell instead of randomly changing node values")
506
    args = parser.parse_args()
507
    logging.basicConfig(format="%(levelname)s: %(message)s", level=getattr(logging, args.loglevel))
508
509
    server = Server()
510
    server.set_endpoint(args.url)
511
    server.disable_clock(args.disable_clock)
512
    server.set_server_name("FreeOpcUa Example Server")
513
    if args.xml:
514
        server.import_xml(args.xml)
515
    if args.populate:
516
        @uamethod
517
        def multiply(parent, x, y):
518
            print("multiply method call with parameters: ", x, y)
519
            return x * y
520
521
        uri = "http://examples.freeopcua.github.io"
522
        idx = server.register_namespace(uri)
523
        objects = server.get_objects_node()
524
        myobj = objects.add_object(idx, "MyObject")
525
        mywritablevar = myobj.add_variable(idx, "MyWritableVariable", 6.7)
526
        mywritablevar.set_writable()    # Set MyVariable to be writable by clients
527
        myvar = myobj.add_variable(idx, "MyVariable", 6.7)
528
        myarrayvar = myobj.add_variable(idx, "MyVarArray", [6.7, 7.9])
529
        myprop = myobj.add_property(idx, "MyProperty", "I am a property")
530
        mymethod = myobj.add_method(idx, "MyMethod", multiply, [ua.VariantType.Double, ua.VariantType.Int64], [ua.VariantType.Double])
531
532
    server.start()
533
    try:
534
        if args.shell:
535
            embed()
536
        else:
537
            count = 0
538
            while True:
539
                time.sleep(1)
540
                myvar.set_value(math.sin(count / 10))
541
                myarrayvar.set_value([math.sin(count / 10), math.sin(count / 100)])
542
                count += 1
543
    finally:
544
        server.stop()
545
    sys.exit(0)
546
547
548
def uadiscover():
549
    parser = argparse.ArgumentParser(description="Performs OPC UA discovery and prints information on servers and endpoints.")
550
    add_minimum_args(parser)
551
    parser.add_argument("-n",
552
                        "--network",
553
                        action="store_true",
554
                        help="Also send a FindServersOnNetwork request to server")
555
    #parser.add_argument("-s",
556
                        #"--servers",
557
                        #action="store_false",
558
                        #help="send a FindServers request to server")
559
    #parser.add_argument("-e",
560
                        #"--endpoints",
561
                        #action="store_false",
562
                        #help="send a GetEndpoints request to server")
563
    args = parse_args(parser)
564
    
565
    client = Client(args.url, timeout=args.timeout)
566
567
    if args.network:
568
        print("Performing discovery at {}\n".format(args.url))
569
        for i, server in enumerate(client.connect_and_find_servers_on_network(), start=1):
570
            print('Server {}:'.format(i))
571
            #for (n, v) in application_to_strings(server):
572
                #print('  {}: {}'.format(n, v))
573
            print('')
574
575
    print("Performing discovery at {}\n".format(args.url))
576
    for i, server in enumerate(client.connect_and_find_servers(), start=1):
577
        print('Server {}:'.format(i))
578
        for (n, v) in application_to_strings(server):
579
            print('  {}: {}'.format(n, v))
580
        print('')
581
582
    for i, ep in enumerate(client.connect_and_get_server_endpoints(), start=1):
583
        print('Endpoint {}:'.format(i))
584
        for (n, v) in endpoint_to_strings(ep):
585
            print('  {}: {}'.format(n, v))
586
        print('')
587
588
    sys.exit(0)
589
590
591
def print_history(o):
592
    if isinstance(o, ua.HistoryData):
593
        print("{:30} {:10} {}".format('Source timestamp', 'Status', 'Value'))
594
        for d in o.DataValues:
595
            print("{:30} {:10} {}".format(str(d.SourceTimestamp), d.StatusCode.name, d.Value))
596
597
598
def str_to_datetime(s):
599
    if not s:
600
        return datetime.utcnow()
601
    # try different datetime formats
602
    for fmt in ["%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S"]:
603
        try:
604
            return datetime.strptime(s, fmt)
605
        except ValueError:
606
            pass
607
608
609
def uahistoryread():
610
    parser = argparse.ArgumentParser(description="Read history of a node")
611
    add_common_args(parser)
612
    parser.add_argument("--starttime",
613
                        default="",
614
                        help="Start time, formatted as YYYY-MM-DD [HH:MM[:SS]]. Default: current time")
615
    parser.add_argument("--endtime",
616
                        default="",
617
                        help="End time, formatted as YYYY-MM-DD [HH:MM[:SS]]. Default: current time")
618
619
    args = parse_args(parser, requirenodeid=True)
620
621
    client = Client(args.url, timeout=args.timeout, security_policy=args.security)
622
    client.connect()
623
    try:
624
        node = get_node(client, args)
625
        starttime = str_to_datetime(args.starttime)
626
        endtime = str_to_datetime(args.endtime)
627
        print("Reading raw history of node {} at {}; start at {}, end at {}\n".format(node, args.url, starttime, endtime))
628
        print_history(node.read_raw_history(starttime, endtime))
629
    finally:
630
        client.disconnect()
631
    sys.exit(0)
632