Пример #1
0
def logix_performance(repeat=1000):
    """Characterize the performance of the logix module, parsing and processing a large request, and
    then parsing the reply.  No network I/O is involved.

    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start
    Obj = logix.Logix(instance_id=1)

    size = 1000
    Obj_a1 = Obj.attribute['1'] = enip.device.Attribute(
        'Something', enip.parser.INT, default=[n for n in range(size)])

    assert len(Obj_a1) == size

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['SCADA'] = {
        'class': Obj.class_id,
        'instance': Obj.instance_id,
        'attribute': 1
    }

    # Lets get it to parse a request, resulting in a 200 element response:
    #     'service': 			0x52,
    #     'path.segment': 		[{'symbolic': 'SCADA', 'length': 5}],
    #     'read_frag.elements':		201,
    #     'read_frag.offset':		2,

    req_1 = bytes(
        bytearray([
            0x52,
            0x04,
            0x91,
            0x05,
            0x53,
            0x43,
            0x41,
            0x44,  #/* R...SCAD */
            0x41,
            0x00,
            0xC9,
            0x00,
            0x02,
            0x00,
            0x00,
            0x00,  #/* A....... */
        ]))

    proc, req_data, rpy_data = False, None, None
    while repeat > 0:
        proc, req_data, rpy_data = logix_test_once(Obj, req_1)
        repeat -= 1

    assert rpy_data.status == 0
    assert len(rpy_data.read_frag.data) == 200
    assert rpy_data.read_frag.data[0] == 1
    assert rpy_data.read_frag.data[-1] == 200
Пример #2
0
def test_logix_remote_pylogix( count=100 ):
    """Performance of pylogix executing an operation a number of times on a socket connected
    Logix simulator, within the same Python interpreter (ie. all on a single CPU
    thread).  Only connects on the standard port.

    """
    #logging.getLogger().setLevel( logging.NORMAL )
    enip.lookup_reset() # Flush out any existing CIP Objects for a fresh start
    svraddr		        = ('localhost', 44828)
    kwargs			= {
        'argv': [
            #'-v',
            #'--log',		'/tmp/pylogix.log',
            #'--profile',	'/tmp/plogix.prof',
            '--address',	'%s:%d' % svraddr,
            'SCADA=INT[1000]'
        ],
        'server': {
            'control': cpppo.apidict( enip.timeout, { 
                'done': False
            } ),
        },
    }
    logixthread_kwargs		= {
        'count':		count,
        'svraddr':		svraddr,
        'kwargs':		kwargs
    }

    log.normal( "test_logix_remote_pylogix w/ server.control in object %s", id( kwargs['server']['control'] ))
    # This is sort of "inside-out".  This thread will run logix_remote, which will signal the
    # enip_main (via the kwargs.server...) to shut down.  However, to do line-based performance
    # measurement, we need to be running enip.main in the "Main" thread...
    logixthread			= threading.Thread( target=logix_remote_pylogix, kwargs=logixthread_kwargs )
    logixthread.daemon		= True
    logixthread.start()

    try:
        enip_main( **kwargs )
    finally:
        kwargs['server']['control'].done = True # Signal the server to terminate

    logixthread.join()
    log.normal( "Shutdown of server complete" )
Пример #3
0
def test_logix_remote( count=50 ):
    """Performance of executing an operation a number of times on a socket connected
    Logix simulator, within the same Python interpreter (ie. all on a single CPU
    thread).

    """
    #logging.getLogger().setLevel( logging.NORMAL )
    enip.lookup_reset() # Flush out any existing CIP Objects for a fresh start
    svraddr		        = ('localhost', 12345)
    kwargs			= {
        'argv': [
            #'-v',
            #'--log',		'/tmp/logix.log',
            #'--profile',	'/tmp/logix.prof',
            '--address',	'%s:%d' % svraddr,
            'SCADA=INT[1000]'
        ],
        'server': {
            'control': cpppo.apidict( enip.timeout, { 
                'done': False
            } ),
        },
    }
    logixthread_kwargs		= {
        'count':		count,
        'svraddr':		svraddr,
        'kwargs':		kwargs
    }

    log.normal( "test_logix_remote w/ server.control in object %s", id( kwargs['server']['control'] ))
    # This is sort of "inside-out".  This thread will run logix_remote, which will signal the
    # enip.main (via the kwargs.server...) to shut down.  However, to do line-based performance
    # measurement, we need to be running enip.main in the "Main" thread...
    logixthread			= threading.Thread( target=logix_remote, kwargs=logixthread_kwargs )
    logixthread.daemon		= True
    logixthread.start()

    enip.main( **kwargs )

    logixthread.join()
Пример #4
0
def logix_performance( repeat=1000 ):
    """Characterize the performance of the logix module, parsing and processing a large request, and
    then parsing the reply.  No network I/O is involved.

    """
    enip.lookup_reset() # Flush out any existing CIP Objects for a fresh start
    Obj				= logix.Logix( instance_id=1 )

    size			= 1000
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'Something', enip.parser.INT, default=[n for n in range( size )])

    assert len( Obj_a1 ) == size

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['SCADA']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }

    # Lets get it to parse a request, resulting in a 200 element response:
    #     'service': 			0x52,
    #     'path.segment': 		[{'symbolic': 'SCADA', 'length': 5}],
    #     'read_frag.elements':		201,
    #     'read_frag.offset':		2,

    req_1	 		= bytes(bytearray([
        0x52, 0x04, 0x91, 0x05, 0x53, 0x43, 0x41, 0x44, #/* R...SCAD */
        0x41, 0x00, 0xC9, 0x00, 0x02, 0x00, 0x00, 0x00, #/* A....... */
    ]))

    proc,req_data,rpy_data	= False,None,None
    while repeat > 0:
        proc,req_data,rpy_data	= logix_test_once( Obj, req_1 )
        repeat		       -= 1

    assert rpy_data.status == 0
    assert len( rpy_data.read_frag.data ) == 200
    assert rpy_data.read_frag.data[ 0] == 1
    assert rpy_data.read_frag.data[-1] == 200
Пример #5
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start
    Obj = logix.Logix(instance_id=1)

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size = 1000
    Obj_a1 = Obj.attribute['1'] = enip.device.Attribute(
        'parts',
        enip.parser.DINT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a2 = Obj.attribute['2'] = enip.device.Attribute('ControlWord',
                                                        enip.parser.DINT,
                                                        default=[0, 0])
    Obj_a3 = Obj.attribute['3'] = enip.device.Attribute(
        'SCADA_40001',
        enip.parser.INT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a4 = Obj.attribute['4'] = enip.device.Attribute('number',
                                                        enip.parser.REAL,
                                                        default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts'] = {
        'class': Obj.class_id,
        'instance': Obj.instance_id,
        'attribute': 1
    }
    enip.device.symbol['ControlWord'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }

    assert len(Obj_a1) == size
    assert len(Obj_a3) == size
    assert len(Obj_a4) == 1
    Obj_a1[0] = 42
    Obj_a2[0] = 476
    Obj_a4[0] = 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description, original, produced, parsed, result, response in GA_tests:
        request = cpppo.dotdict(original)

        log.warning("%s; request: %s", description, enip.enip_format(request))
        encoded = Obj.produce(request)
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced)

        # Now, use the Message_Router's parser to decode the encoded bytes
        source = cpppo.rememberable(encoded)
        decoded = cpppo.dotdict()
        with Obj.parser as machine:
            for m, s in enumerate(machine.run(source=source, data=decoded)):
                pass
        for k, v in cpppo.dotdict(parsed).items():
            assert decoded[
                k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                    description, k, v, enip.enip_format(decoded))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request(request)
        logging.warning("%s: reply:   %s", description,
                        enip.enip_format(request))
        for k, v in cpppo.dotdict(result).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded = Obj.produce(request)
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response)

    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.read_frag = {}
    data.read_frag.elements = 1000
    data.read_frag.offset = 0

    # Reply maximum size limited
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 0 and end == 125 and endactual == 1000  # DINT == 4 bytes
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 250 and endactual == 1000  # INT == 2 bytes

    data.read_frag.offset = 125 * 4  # OK, second request; begin after byte offset of first
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 125 and end == 250 and endactual == 1000  # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements = 30
    data.read_frag.offset = 0
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 30 and endactual == 30  # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements = 70
    data.read_frag.offset = 80
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 40 and end == 70 and endactual == 70  # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.write_frag = {}
    data.write_frag.data = [0] * 100  # 100 elements provided in this request
    data.write_frag.elements = 200  # Total request is to write 200 elements
    data.write_frag.offset = 16  # request starts 16 bytes in (8 INTs)
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 and end == 108 and endactual == 200  # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 222
            },
        ]]
    }
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 + 222 and end == 108 + 222 and endactual == 200 + 222  # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {'segment': []}

    data.write_frag = {}
    data.write_frag.data = [3, 4, 5, 6]
    data.write_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 3 and end == 7 and endactual == 1000  # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset = (1000 - 3) * 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str(exc)

    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {'segment': []}

    data.read_frag = {}
    data.read_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 3 and end == 253 and endactual == 1000  # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset = 1998
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 999 and end == 1000 and endactual == 1000  # INT == 2 bytes

    # Trigger all the remaining error cases

    # Unknown service
    data.service = Obj.RD_FRG_REQ
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str(exc)

    # Offset indivisible by element size
    data.service = Obj.RD_FRG_RPY
    data.read_frag.offset = 7
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str(exc)

    # Initial element outside bounds
    data.read_frag.offset = 2000
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str(exc)

    # Ending element outside bounds
    data.read_frag.offset = 0
    data.read_frag.elements = 1001
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str(exc)

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset = 6
    data.read_frag.elements = 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str(exc)
    data.read_frag.offset = 2
    data.read_frag.elements = 2
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 1 and end == 2 and endactual == 2  # INT == 2 bytes

    # Test an example valid multiple request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = [
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict()
    ]
    req = data.multiple.request

    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'parts'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1

    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'ControlWord'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1

    req[2].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[2].read_tag = {}
    req[2].read_tag.elements = 1

    req[3].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[3].write_tag = {}
    req[3].write_tag.elements = 1
    req[3].write_tag.type = 0x00ca
    req[3].write_tag.data = [1.25]

    req[4].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[4].read_tag = {}
    req[4].read_tag.elements = 1

    request = Obj.produce(data)

    req_1 = bytes(
        bytearray([
            0x0A,
            0x02,
            0x20,
            0x02,
            0x24,
            0x01,
            0x05,
            0x00,
            0x0c,
            0x00,
            0x18,
            0x00,
            0x2a,
            0x00,
            0x36,
            0x00,
            0x48,
            0x00,
            0x4C,
            0x04,
            0x91,
            0x05,
            0x70,
            0x61,
            0x72,
            0x74,
            0x73,
            0x00,
            0x01,
            0x00,
            0x4C,
            0x07,
            0x91,
            0x0B,
            0x43,
            0x6F,
            0x6E,
            0x74,
            0x72,
            0x6F,
            0x6C,
            0x57,
            0x6F,
            0x72,
            0x64,
            0x00,
            0x01,
            0x00,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
            b'M'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0xca,
            0x00,
            0x01,
            0x00,
            0x00,
            0x00,
            0xa0,
            0x3f,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
        ]))

    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source = cpppo.rememberable(request)
    data = cpppo.dotdict()
    with Obj.parser as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            pass
    log.normal("Multiple Request: %s", enip.enip_format(data))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce(data) == req_1

    # Process the request into a reply.
    Obj.request(data)
    log.normal("Multiple Response: %s", enip.enip_format(data))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1 = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x05,
        0x00,
        0x0c,
        0x00,
        0x16,
        0x00,
        0x20,
        0x00,
        0x2a,
        0x00,
        0x2e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0x2A,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0xDC,
        0x01,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xCA,
        0x00,
        0x00,
        0x00,
        0x80,
        0x3F,
        0xcd,
        0x00,
        0x00,
        0x00,
        0xcc,
        0x00,
        0x00,
        0x00,
        0xca,
        0x00,
        0x00,
        0x00,
        0xa0,
        0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_good = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x01,
        0x00,
        0x04,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request(data)
    rpy_good = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x01,
        0x00,
        0x04,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict(), cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40002'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_bad = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x02,
        0x00,
        0x06,
        0x00,
        0x18,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('2'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request(data)
    rpy_bad = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x02,
        0x00,
        0x06,
        0x00,
        0x0e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x05,
        0x01,  # Status code 0x05 (invalid path)
        0x00,
        0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Пример #6
0
def test_CIP_HART(repeat=1):
    """HART protocol enip CIP messages
    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start

    ENIP = enip.enip_machine(context='enip')
    CIP = enip.CIP()
    # We'll use a HART Message Router, to handle its expanded porfolio of commands
    MR = HART(instance_id=1)

    for pkt, tst in client.recycle(CIP_HART_tests, times=repeat):
        # Parse just the CIP portion following the EtherNet/IP encapsulation header
        data = cpppo.dotdict()
        source = cpppo.chainable(pkt)
        with ENIP as machine:
            for i, (m, s) in enumerate(machine.run(source=source, data=data)):
                log.detail("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                           machine.name_centered(), i, s, source.sent,
                           source.peek(), data)
        # In a real protocol implementation, an empty header (EOF with no input at all) is
        # acceptable; it indicates a session closed by the client.
        if not data:
            log.normal("EtherNet/IP Request: Empty (session terminated): %s",
                       enip.enip_format(data))
            continue

        if log.isEnabledFor(logging.NORMAL):
            log.normal("EtherNet/IP Request: %s", enip.enip_format(data))

        # Parse the encapsulated .input
        with CIP as machine:
            for i, (m, s) in enumerate(
                    machine.run(path='enip',
                                source=cpppo.peekable(
                                    data.enip.get('input', b'')),
                                data=data)):
                log.detail("%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                           machine.name_centered(), i, s, source.sent,
                           source.peek(), data)

        if log.isEnabledFor(logging.NORMAL):
            log.normal("EtherNet/IP CIP Request: %s", enip.enip_format(data))

        # Assume the request in the CIP's CPF items are HART requests.
        # Now, parse the encapsulated message(s).  We'll assume it is destined for a HART Object.
        if 'enip.CIP.send_data' in data:
            for item in data.enip.CIP.send_data.CPF.item:
                if 'unconnected_send.request' in item:
                    # An Unconnected Send that contained an encapsulated request (ie. not just a Get
                    # Attribute All)
                    with MR.parser as machine:
                        if log.isEnabledFor(logging.NORMAL):
                            log.normal(
                                "Parsing %3d bytes using %s.parser, from %s",
                                len(item.unconnected_send.request.input), MR,
                                enip.enip_format(item))
                        # Parse the unconnected_send.request.input octets, putting parsed items into the
                        # same request context
                        for i, (m, s) in enumerate(
                                machine.run(
                                    source=cpppo.peekable(
                                        item.unconnected_send.request.input),
                                    data=item.unconnected_send.request)):
                            log.detail(
                                "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r",
                                machine.name_centered(), i, s, source.sent,
                                source.peek(), data)
                    # Post-processing of some parsed items is only performed after lock released!
                    if log.isEnabledFor(logging.NORMAL):
                        log.normal(
                            "Parsed  %3d bytes using %s.parser, into %s",
                            len(item.unconnected_send.request.input), MR,
                            enip.enip_format(data))
        try:
            for k, v in tst.items():
                assert data[k] == v, ("data[%r] == %r\n"
                                      "expected:   %r" % (k, data[k], v))
        except:
            log.warning("%r not in data, or != %r: %s", k, v,
                        enip.enip_format(data))
            raise

        # Ensure that we can get the original EtherNet/IP CIP back
        for k in list(data.keys()):
            if k.endswith('input') and 'sender_context' not in k:
                log.detail("del data[%r]", k)
                del data[k]
        try:
            # First reconstruct any SendRRData CPF items, containing encapsulated requests/responses
            if 'enip.CIP.send_data' in data:
                cpf = data.enip.CIP.send_data
                for item in cpf.CPF.item:
                    if 'unconnected_send' in item:
                        item.unconnected_send.request.input = bytearray(
                            MR.produce(item.unconnected_send.request))
                        log.normal("Produce HART message from: %r",
                                   item.unconnected_send.request)

            # Next, reconstruct the CIP Register, ListIdentity, ListServices, or SendRRData.  The CIP.produce must
            # be provided the EtherNet/IP header, because it contains data (such as .command)
            # relevant to interpreting the .CIP... contents.
            data.enip.input = bytearray(enip.CIP.produce(data.enip))
            # And finally the EtherNet/IP encapsulation itself
            data.input = bytearray(enip.enip_encode(data.enip))
            log.detail("EtherNet/IP CIP Request produced payload: %r",
                       bytes(data.input))
            assert data.input == pkt, "original:\n" + hexdump(
                pkt) + "\nproduced:\n" + hexdump(data.input)
        except Exception as exc:
            log.warning(
                "Exception %s; Invalid packet produced from EtherNet/IP CIP data: %s",
                exc, enip.enip_format(data))
            raise
Пример #7
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    enip.lookup_reset() # Flush out any existing CIP Objects for a fresh start
    Obj				= logix.Logix( instance_id=1 )

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size			= 1000
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'parts',       enip.parser.DINT, default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a2 = Obj.attribute['2']	= enip.device.Attribute( 'ControlWord', enip.parser.DINT, default=[0,0])
    Obj_a3 = Obj.attribute['3']	= enip.device.Attribute( 'SCADA_40001', enip.parser.INT,  default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a4 = Obj.attribute['4']	= enip.device.Attribute( 'number',      enip.parser.REAL, default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }
    enip.device.symbol['ControlWord'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }


    assert len( Obj_a1 ) == size
    assert len( Obj_a3 ) == size
    assert len( Obj_a4 ) == 1
    Obj_a1[0]			= 42
    Obj_a2[0]			= 476
    Obj_a4[0]			= 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description,original,produced,parsed,result,response in GA_tests:
        request			= cpppo.dotdict( original )

        log.warning( "%s; request: %s", description, enip.enip_format( request ))
        encoded			= Obj.produce( request )
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced )

        # Now, use the Message_Router's parser to decode the encoded bytes
        source			= cpppo.rememberable( encoded )
        decoded			= cpppo.dotdict()
        with Obj.parser as machine:
            for m,s in enumerate( machine.run( source=source, data=decoded )):
                pass
        for k,v in cpppo.dotdict( parsed ).items():
            assert decoded[k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                description, k, v, enip.enip_format( decoded ))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request( request )
        logging.warning("%s: reply:   %s", description, enip.enip_format( request ))
        for k,v in cpppo.dotdict( result ).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded			= Obj.produce( request )
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response )


    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.read_frag		= {}
    data.read_frag.elements	= 1000
    data.read_frag.offset	= 0
    
    # Reply maximum size limited
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 0 and end == 125 and endactual == 1000 # DINT == 4 bytes
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 250 and endactual == 1000 # INT == 2 bytes

    data.read_frag.offset	= 125*4 # OK, second request; begin after byte offset of first
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 125 and end == 250 and endactual == 1000 # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements	= 30
    data.read_frag.offset	= 0
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 30 and endactual == 30 # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements	= 70
    data.read_frag.offset	= 80
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 40 and end == 70 and endactual == 70 # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.write_frag		= {}
    data.write_frag.data	= [0] * 100 # 100 elements provided in this request
    data.write_frag.elements	= 200       # Total request is to write 200 elements
    data.write_frag.offset	= 16        # request starts 16 bytes in (8 INTs)
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8 and end == 108 and endactual == 200 # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 222 },
                                                       ]] }
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8+222 and end == 108+222 and endactual == 200+222 # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [] }

    data.write_frag		= {}
    data.write_frag.data	= [3,4,5,6]
    data.write_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 3 and end == 7 and endactual == 1000 # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset	= ( 1000 - 3 ) * 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'write_frag' )
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str( exc )

    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [] }

    data.read_frag		= {}
    data.read_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 3 and end == 253 and endactual == 1000 # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset	= 1998
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 999 and end == 1000 and endactual == 1000 # INT == 2 bytes


    # Trigger all the remaining error cases

    # Unknown service
    data.service		= Obj.RD_FRG_REQ
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str( exc )

    # Offset indivisible by element size
    data.service		= Obj.RD_FRG_RPY
    data.read_frag.offset	= 7
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str( exc )

    # Initial element outside bounds
    data.read_frag.offset	= 2000
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str( exc )

    # Ending element outside bounds
    data.read_frag.offset	= 0
    data.read_frag.elements	= 1001
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str( exc )

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset	= 6
    data.read_frag.elements	= 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str( exc )
    data.read_frag.offset	= 2
    data.read_frag.elements	= 2
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 1 and end == 2 and endactual == 2 # INT == 2 bytes


    # Test an example valid multiple request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request	= [
        cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict() ] 
    req				= data.multiple.request

    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'parts'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'ControlWord'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1

    req[2].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[2].read_tag		= {}
    req[2].read_tag.elements	= 1

    req[3].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[3].write_tag		= {}
    req[3].write_tag.elements	= 1
    req[3].write_tag.type	= 0x00ca
    req[3].write_tag.data	= [1.25]

    req[4].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[4].read_tag		= {}
    req[4].read_tag.elements	= 1


    request			= Obj.produce( data )

    req_1			= bytes(bytearray([
        0x0A,
        0x02,
        0x20, 0x02, 0x24, 0x01,
        
        0x05, 0x00,
        
        0x0c, 0x00,
        0x18, 0x00,
        0x2a, 0x00,
        0x36, 0x00,
        0x48, 0x00,
        
        0x4C,
        0x04, 0x91, 0x05, 0x70, 0x61,
        0x72, 0x74, 0x73, 0x00,
        0x01, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0B, 0x43, 0x6F,
        0x6E, 0x74, 0x72, 0x6F, 0x6C,
        0x57, 0x6F, 0x72, 0x64, 0x00,
        0x01, 0x00,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,

        b'M'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0xca, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa0, 0x3f,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,
    ]))
                       
    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source			= cpppo.rememberable( request )
    data			= cpppo.dotdict()
    with Obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            pass
    log.normal( "Multiple Request: %s", enip.enip_format( data ))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce( data ) == req_1

    # Process the request into a reply.
    Obj.request( data )
    log.normal( "Multiple Response: %s", enip.enip_format( data ))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x05, 0x00,

        0x0c, 0x00,
        0x16, 0x00,
        0x20, 0x00,
        0x2a, 0x00,
        0x2e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0x2A, 0x00, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0xDC, 0x01, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xCA, 0x00, 0x00, 0x00, 0x80, 0x3F,

        0xcd, 0x00, 0x00, 0x00,

        0xcc, 0x00, 0x00, 0x00,
        0xca, 0x00, 0x00, 0x00, 0xa0, 0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_good			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x01, 0x00,
        
        0x04, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request( data )
    rpy_good			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x01, 0x00,

        0x04, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40002'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_bad			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x18, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,

        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('2'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request( data )
    rpy_bad			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x0e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,

        0xCC, 0x00, 0x05, 0x01, # Status code 0x05 (invalid path)
        0x00, 0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )