예제 #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.

    """
    Obj = enip.device.lookup(enip.device.Message_Router.class_id,
                             instance_id=1)
    if not isinstance(Obj, logix.Logix):
        if Obj is not None:
            del enip.device.directory['2']['1']
        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
파일: client.py 프로젝트: gregwjacobs/cpppo
 def __init__(self, host, port=None, timeout=None):
     """Connect to the EtherNet/IP client, waiting  """
     self.addr = (host or address[0], port or address[1])
     self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     self.conn.connect(self.addr)
     self.session = None
     self.source = cpppo.chainable()
     self.data = None
     # Parsers
     self.engine = None  # EtherNet/IP frame parsing in progress
     self.frame = enip.enip_machine(terminal=True)
     self.cip = enip.CIP(
         terminal=True)  # Parses a CIP   request in an EtherNet/IP frame
     self.lgx = logix.Logix(
     ).parser  # Parses a Logix request in an EtherNet/IP CIP request
예제 #3
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 )
예제 #4
0
def logix_performance(repeat=1000):
    """Characterize the performance of the logix module."""
    size = 1000
    Obj = logix.Logix()
    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:
    #     'service': 			0x52,
    #     'path.segment': 		[{'symbolic': 'SCADA', 'length': 5}],
    #     'read_frag.elements':		20,
    #     'read_frag.offset':		2,

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

    def test_once():
        source = cpppo.peekable(req_1)
        data = cpppo.dotdict()
        with Obj.parser as machine:
            for m, w in machine.run(source=source, data=data):
                pass
        log.normal("Logix Request parsed: %s", enip.enip_format(data))

        # If we ask a Logix Object to process the request, it should respond.
        processed = Obj.request(data)
        log.normal("Logix Request processed: %s", enip.enip_format(data))
        return processed, data

    processed, data = False, None
    while repeat > 0:
        processed, data = test_once()
        repeat -= 1

    assert data.status == 0
    assert len(data.read_frag.data) == 20
    assert data.read_frag.data[0] == 1
    assert data.read_frag.data[-1] == 20