Exemple #1
0
    def start(self, host, port):
        srv_ctl = cpppo.dotdict()
        srv_ctl.control = cpppo.apidict(timeout=self.config.timeout)
        srv_ctl.control["done"] = False
        srv_ctl.control["disable"] = False
        srv_ctl.control.setdefault("latency", self.config.latency)

        options = cpppo.dotdict()
        options.setdefault("enip_process", logix.process)
        kwargs = dict(options, tags=self.tags, server=srv_ctl)

        tcp_mode = True if self.config.mode == "tcp" else False
        udp_mode = True if self.config.mode == "udp" else False

        self.control = srv_ctl.control
        self.start_event.set()

        logger.debug(
            "ENIP server started on: %s:%d, mode: %s" % (host, port, self.config.mode)
        )
        while not self.control["done"]:
            network.server_main(
                address=(host, port),
                target=self.handle,
                kwargs=kwargs,
                udp=udp_mode,
                tcp=tcp_mode,
            )
Exemple #2
0
    def start(self, host, port):
        srv_ctl = cpppo.dotdict()
        srv_ctl.control = cpppo.apidict(timeout=self.config.timeout)
        srv_ctl.control['done'] = False
        srv_ctl.control['disable'] = False
        srv_ctl.control.setdefault('latency', self.config.latency)

        options = cpppo.dotdict()
        options.setdefault('enip_process', logix.process)
        kwargs = dict(options, tags=self.tags, server=srv_ctl)

        tcp_mode = True if self.config.mode == 'tcp' else False
        udp_mode = True if self.config.mode == 'udp' else False

        logger.debug('ENIP server started on: %s:%d, mode: %s' % (host, port, self.config.mode))
        while not self.stopped:
            network.server_main(address=(host, port), target=self.handle,
                                kwargs=kwargs, idle_service=None,
                                udp=udp_mode, tcp=tcp_mode,
                                thread_factory=network.server_thread)
Exemple #3
0
def main( argv=None ):
    ap				= argparse.ArgumentParser(
        description = "TNET Network Client",
        epilog = "" )

    ap.add_argument( '-v', '--verbose', action="count",
                     default=0, 
                     help="Display logging information." )
    ap.add_argument( '-l', '--log',
                     help="Log file, if desired" )
    ap.add_argument( '-T', '--timeout', default=None,
                     help="Optional timeout on receiving TNET string; responds w/ None upon timeout" )
    ap.add_argument( '-L', '--latency', default=None,
                     help="Optional latency on checking server.done" )
    ap.add_argument( '-a', '--address', default=None,
                     help="The local interface[:port] to bind to (default: {iface}:{port})".format(
                         iface=address[0], port=address[1] ))
    args			= ap.parse_args( argv )
    
    idle_service		= []

    # Set up logging level (-v...) and --log <file>, handling log-file rotation
    levelmap 			= {
        0: logging.WARNING,
        1: logging.NORMAL,
        2: logging.DETAIL,
        3: logging.INFO,
        4: logging.DEBUG,
        }
    cpppo.log_cfg['level']	= ( levelmap[args.verbose] 
                                    if args.verbose in levelmap
                                    else logging.DEBUG )
    if args.log:
        cpppo.log_cfg['filename']= args.log
 
    logging.basicConfig( **cpppo.log_cfg )

    timeout			= None if args.timeout is None else float( args.timeout )
    latency			= None if args.latency is None else float( args.latency )

    bind			= address
    if args.address:
        host,port		= cpppo.parse_ip_port( args.address, default=address )
        bind			= str(host),int(port)

    return network.server_main(
        address	= bind,
        target	= tnet_server_json,
        kwargs	= dict(
            timeout	= timeout,
            latency	= latency,
            ignore	= b'\n'
        )
    )
Exemple #4
0
def main( argv=None, attribute_class=device.Attribute, identity_class=None, idle_service=None,
          **kwds ):
    """Pass the desired argv (excluding the program name in sys.arg[0]; typically pass argv=None, which
    is equivalent to argv=sys.argv[1:], the default for argparse.  Requires at least one tag to be
    defined.

    If a cpppo.apidict() is passed for kwds['server']['control'], we'll use it to transmit server
    control signals via its .done, .disable, .timeout and .latency attributes.

    Uses the provided attribute_class (default: device.Attribute) to process all EtherNet/IP
    attribute I/O (eg. Read/Write Tag [Fragmented]) requests.  By default, device.Attribute stores
    and retrieves the supplied data.  To perform other actions (ie. forward the data to your own
    application), derive from device.Attribute, and override the __getitem__ and __setitem__
    methods.

    If an idle_service function is provided, it will be called after a period of latency between
    incoming requests.

    """
    global address
    global options
    global tags
    global srv_ctl
    global latency
    global timeout

    ap				= argparse.ArgumentParser(
        description = "Provide an EtherNet/IP Server",
        epilog = "" )

    ap.add_argument( '-v', '--verbose',
                     default=0, action="count",
                     help="Display logging information." )
    ap.add_argument( '-a', '--address',
                     default=( "%s:%d" % address ),
                     help="EtherNet/IP interface[:port] to bind to (default: %s:%d)" % (
                         address[0], address[1] ))
    ap.add_argument( '-p', '--print', default=False, action='store_true',
                     help="Print a summary of operations to stdout" )
    ap.add_argument( '-l', '--log',
                     help="Log file, if desired" )
    ap.add_argument( '-w', '--web',
                     default="",
                     help="Web API [interface]:[port] to bind to (default: %s, port 80)" % (
                         address[0] ))
    ap.add_argument( '-d', '--delay',
                     help="Delay response to each request by a certain number of seconds (default: 0.0)",
                     default="0.0" )
    ap.add_argument( '-s', '--size',
                     help="Limit EtherNet/IP encapsulated request size to the specified number of bytes (default: None)",
                     default=None )
    ap.add_argument( '-P', '--profile',
                     help="Output profiling data to a file (default: None)",
                     default=None )
    ap.add_argument( 'tags', nargs="+",
                     help="Any tags, their type (default: INT), and number (default: 1), eg: tag=INT[1000]")

    args			= ap.parse_args( argv )

    # Deduce interface:port address to bind, and correct types (default is address, above)
    bind			= args.address.split(':')
    assert 1 <= len( bind ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address
    bind			= ( str( bind[0] ) if bind[0] else address[0],
                                    int( bind[1] ) if len( bind ) > 1 and bind[1] else address[1] )

    # Set up logging level (-v...) and --log <file>
    levelmap 			= {
        0: logging.WARNING,
        1: logging.NORMAL,
        2: logging.DETAIL,
        3: logging.INFO,
        4: logging.DEBUG,
        }
    cpppo.log_cfg['level']	= ( levelmap[args.verbose] 
                                    if args.verbose in levelmap
                                    else logging.DEBUG )

    # Chain any provided idle_service function with log rotation; these may (also) consult global
    # signal flags such as logrotate_request, so execute supplied functions before logrotate_perform
    idle_service		= [ idle_service ] if idle_service else []
    if args.log:
        # Output logging to a file, and handle UNIX-y log file rotation via 'logrotate', which sends
        # signals to indicate that a service's log file has been moved/renamed and it should re-open
        cpppo.log_cfg['filename']= args.log
        signal.signal( signal.SIGHUP, logrotate_request )
        idle_service.append( logrotate_perform )

    logging.basicConfig( **cpppo.log_cfg )


    # Pull out a 'server.control...' supplied in the keywords, and make certain it's a
    # cpppo.apidict.  We'll use this to transmit control signals to the server thread.  Set the
    # current values to sane initial defaults/conditions.
    if 'server' in kwds:
        assert 'control' in kwds['server'], "A 'server' keyword provided without a 'control' attribute"
        srv_ctl			= cpppo.dotdict( kwds.pop( 'server' ))
        assert isinstance( srv_ctl['control'], cpppo.apidict ), "The server.control... must be a cpppo.apidict"
    else:
        srv_ctl.control		= cpppo.apidict( timeout=timeout )

    srv_ctl.control['done']	= False
    srv_ctl.control['disable']	= False
    srv_ctl.control.setdefault( 'latency', latency )

    # Global options data.  Copy any remaining keyword args supplied to main().  This could
    # include an alternative enip_process, for example, instead of defaulting to logix.process.
    options.update( kwds )

    # Specify a response delay.  The options.delay is another dotdict() layer, so it's attributes
    # (eg. .value, .range) are available to the web API for manipulation.  Therefore, they can be
    # set to arbitrary values at random times!  However, the type will be retained.
    def delay_range( *args, **kwds ):
        """If a delay.range like ".1-.9" is specified, then change the delay.value every second to something
        in that range."""
        assert 'delay' in kwds and 'range' in kwds['delay'] and '-' in kwds['delay']['range'], \
            "No delay=#-# specified"
        log.normal( "Delaying all responses by %s seconds", kwds['delay']['range'] )
        while True:
            # Once we start, changes to delay.range will be re-evaluated each loop
            time.sleep( 1 )
            try:
                lo,hi		= map( float, kwds['delay']['range'].split( '-' ))
                kwds['delay']['value'] = random.uniform( lo, hi )
                log.info( "Mutated delay == %g", kwds['delay']['value'] )
            except Exception as exc:
                log.warning( "No delay=#[.#]-#[.#] range specified: %s", exc )

    options.delay		= cpppo.dotdict()
    try:
        options.delay.value	= float( args.delay )
        log.normal( "Delaying all responses by %r seconds" , options.delay.value )
    except:
        assert '-' in args.delay, \
            "Unrecognized --delay=%r option" % args.delay
        # A range #-#; set up a thread to mutate the option.delay.value over the .range
        options.delay.range	= args.delay
        options.delay.value	= 0.0
        mutator			= threading.Thread( target=delay_range, kwargs=options )
        mutator.daemon		= True
        mutator.start()

    # Create all the specified tags/Attributes.  The enip_process function will (somehow) assign the
    # given tag name to reference the specified Attribute.  We'll define an Attribute to print
    # I/O if args.print is specified; reads will only be logged at logging.NORMAL and above.
    class Attribute_print( attribute_class ):
        def __getitem__( self, key ):
            value		= super( Attribute_print, self ).__getitem__( key )
            if log.isEnabledFor( logging.NORMAL ):
                print( "%20s[%5s-%-5s] == %s" % (
                    self.name, 
                    key.indices( len( self ))[0]   if isinstance( key, slice ) else key,
                    key.indices( len( self ))[1]-1 if isinstance( key, slice ) else key,
                    value ))
            return value

        def __setitem__( self, key, value ):
            super( Attribute_print, self ).__setitem__( key, value )
            print( "%20s[%5s-%-5s] <= %s" % (
                self.name, 
                key.indices( len( self ))[0]   if isinstance( key, slice ) else key,
                key.indices( len( self ))[1]-1 if isinstance( key, slice ) else key,
                value ))

    for t in args.tags:
        tag_name, rest		= t, ''
        if '=' in tag_name:
            tag_name, rest	= tag_name.split( '=', 1 )
        tag_type, rest		= rest or 'INT', ''
        tag_size		= 1
        if '[' in tag_type:
            tag_type, rest	= tag_type.split( '[', 1 )
            assert ']' in rest, "Invalid tag; mis-matched [...]"
            tag_size, rest	= rest.split( ']', 1 )
        assert not rest, "Invalid tag specified; expected tag=<type>[<size>]: %r" % t
        tag_type		= str( tag_type ).upper()
        typenames		= {"INT": parser.INT, "DINT": parser.DINT, "SINT": parser.SINT, "REAL": parser.REAL }
        assert tag_type in typenames, "Invalid tag type; must be one of %r" % list( typenames.keys() )
        tag_default		= 0.0 if tag_type == "REAL" else 0
        try:
            tag_size		= int( tag_size )
        except:
            raise AssertionError( "Invalid tag size: %r" % tag_size )

        # Ready to create the tag and its Attribute (and error code to return, if any).  If tag_size
        # is 1, it will be a scalar Attribute.  Since the tag_name may contain '.', we don't want
        # the normal dotdict.__setitem__ resolution to parse it; use plain dict.__setitem__.
        log.normal( "Creating tag: %s=%s[%d]", tag_name, tag_type, tag_size )
        tag_entry		= cpppo.dotdict()
        tag_entry.attribute	= ( Attribute_print if args.print else attribute_class )(
            tag_name, typenames[tag_type], default=( tag_default if tag_size == 1 else [tag_default] * tag_size ))
        tag_entry.error		= 0x00
        dict.__setitem__( tags, tag_name, tag_entry )

    # Use the Logix simulator by default (unless some other one was supplied as a keyword options to
    # main(), loaded above into 'options').  This key indexes an immutable value (not another
    # dotdict layer), so is not available for the web API to report/manipulate.
    options.setdefault( 'enip_process', logix.process )
    options.setdefault( 'identity_class', identity_class )

    # The Web API

    # Deduce web interface:port address to bind, and correct types (default is address, above).
    # Default to the same interface as we're bound to, port 80.  We'll only start if non-empty --web
    # was provided, though (even if it's just ':', to get all defaults).  Usually you'll want to
    # specify at least --web :[<port>].
    http			= args.web.split(':')
    assert 1 <= len( http ) <= 2, "Invalid --web [<interface>]:[<port>}: %s" % args.web
    http			= ( str( http[0] ) if http[0] else bind[0],
                                    int( http[1] ) if len( http ) > 1 and http[1] else 80 )


    if args.web:
        assert 'web' in sys.modules, "Failed to import web API module; --web option not available.  Run 'pip install web.py'"
        logging.normal( "EtherNet/IP Simulator Web API Server: %r" % ( http, ))
        webserver		= threading.Thread( target=web_api, kwargs={'http': http} )
        webserver.daemon	= True
        webserver.start()

        
    # The EtherNet/IP Simulator.  Pass all the top-level options keys/values as keywords, and pass
    # the entire tags dotdict as a tags=... keyword.  The server_main server.control signals (.done,
    # .disable) are also passed as the server= keyword.  We are using an cpppo.apidict with a long
    # timeout; this will block the web API for several seconds to allow all threads to respond to
    # the signals delivered via the web API.
    logging.normal( "EtherNet/IP Simulator: %r" % ( bind, ))
    kwargs			= dict( options, latency=latency, size=args.size, tags=tags, server=srv_ctl )

    tf				= network.server_thread
    tf_kwds			= dict()
    if args.profile:
        tf			= network.server_thread_profiling
        tf_kwds['filename']	= args.profile

    disabled			= False	# Recognize toggling between en/disabled
    while not srv_ctl.control.done:
        if not srv_ctl.control.disable:
            if disabled:
                logging.detail( "EtherNet/IP Server enabled" )
                disabled= False
            network.server_main( address=bind, target=enip_srv, kwargs=kwargs,
                                 idle_service=lambda: map( lambda f: f(), idle_service ),
                                 thread_factory=tf, **tf_kwds )
        else:
            if not disabled:
                logging.detail( "EtherNet/IP Server disabled" )
                disabled= True
            time.sleep( latency )            # Still disabled; wait a bit

    return 0
Exemple #5
0
def main(argv=None, **kwds):
    """Pass the desired argv (excluding the program name in sys.arg[0]; typically
    pass argv=None, which is equivalent to argv=sys.argv[1:], the default for
    argparse.  Requires at least one tag to be defined.

    If a cpppo.apidict() is passed for kwds['server']['control'], we'll use it
    to transmit server control signals via its .done, .disable, .timeout and
    .latency attributes.

    """
    global address
    global options
    global tags
    global srv_ctl
    global latency
    global timeout

    ap = argparse.ArgumentParser(description="Provide an EtherNet/IP Server",
                                 epilog="")

    ap.add_argument('-v',
                    '--verbose',
                    default=0,
                    action="count",
                    help="Display logging information.")
    ap.add_argument(
        '-a',
        '--address',
        default=("%s:%d" % address),
        help="EtherNet/IP interface[:port] to bind to (default: %s:%d)" %
        (address[0], address[1]))
    ap.add_argument('-l', '--log', help="Log file, if desired")
    ap.add_argument(
        '-w',
        '--web',
        default="",
        help="Web API [interface]:[port] to bind to (default: %s, port 80)" %
        (address[0]))
    ap.add_argument(
        '-d',
        '--delay',
        help=
        "Delay response to each request by a certain number of seconds (default: 0.0)",
        default="0.0")
    ap.add_argument('-p',
                    '--profile',
                    help="Output profiling data to a file (default: None)",
                    default=None)
    ap.add_argument(
        'tags',
        nargs="+",
        help=
        "Any tags, their type (default: INT), and number (default: 1), eg: tag=INT[1000]"
    )

    args = ap.parse_args(argv)

    # Deduce interface:port address to bind, and correct types (default is address, above)
    bind = args.address.split(':')
    assert 1 <= len(
        bind
    ) <= 2, "Invalid --address [<interface>]:[<port>}: %s" % args.address
    bind = (str(bind[0]) if bind[0] else address[0],
            int(bind[1]) if len(bind) > 1 and bind[1] else address[1])

    # Set up logging level (-v...) and --log <file>
    levelmap = {
        0: logging.WARNING,
        1: logging.NORMAL,
        2: logging.DETAIL,
        3: logging.INFO,
        4: logging.DEBUG,
    }
    cpppo.log_cfg['level'] = (levelmap[args.verbose]
                              if args.verbose in levelmap else logging.DEBUG)

    idle_service = None
    if args.log:
        # Output logging to a file, and handle UNIX-y log file rotation via 'logrotate', which sends
        # signals to indicate that a service's log file has been moved/renamed and it should re-open
        cpppo.log_cfg['filename'] = args.log
        signal.signal(signal.SIGHUP, logrotate_request)
        idle_service = logrotate_perform

    logging.basicConfig(**cpppo.log_cfg)

    # Pull out a 'server.control...' supplied in the keywords, and make certain it's a
    # cpppo.apidict.  We'll use this to transmit control signals to the server thread.  Set the
    # current values to sane initial defaults/conditions.
    if 'server' in kwds:
        assert 'control' in kwds[
            'server'], "A 'server' keyword provided without a 'control' attribute"
        srv_ctl = cpppo.dotdict(kwds.pop('server'))
        assert isinstance(
            srv_ctl['control'],
            cpppo.apidict), "The server.control... must be a cpppo.apidict"
    else:
        srv_ctl.control = cpppo.apidict(timeout=timeout)

    srv_ctl.control['done'] = False
    srv_ctl.control['disable'] = False
    srv_ctl.control.setdefault('latency', latency)

    # Global options data.  Copy any remaining keyword args supplied to main().  This could
    # include an alternative enip_process, for example, instead of defaulting to logix.process.
    options.update(kwds)

    # Specify a response delay.  The options.delay is another dotdict() layer, so it's attributes
    # (eg. .value, .range) are available to the web API for manipulation.  Therefore, they can be
    # set to arbitrary values at random times!  However, the type will be retained.
    def delay_range(*args, **kwds):
        """If a delay.range like ".1-.9" is specified, then change the delay.value every second to something
        in that range."""
        assert 'delay' in kwds and 'range' in kwds['delay'] and '-' in kwds['delay']['range'], \
            "No delay=#-# specified"
        log.normal("Delaying all responses by %s seconds",
                   kwds['delay']['range'])
        while True:
            # Once we start, changes to delay.range will be re-evaluated each loop
            time.sleep(1)
            try:
                lo, hi = map(float, kwds['delay']['range'].split('-'))
                kwds['delay']['value'] = random.uniform(lo, hi)
                log.info("Mutated delay == %g", kwds['delay']['value'])
            except Exception as exc:
                log.warning("No delay=#[.#]-#[.#] range specified: %s", exc)

    options.delay = cpppo.dotdict()
    try:
        options.delay.value = float(args.delay)
        log.normal("Delaying all responses by %r seconds", options.delay.value)
    except:
        assert '-' in args.delay, \
            "Unrecognized --delay=%r option" % args.delay
        # A range #-#; set up a thread to mutate the option.delay.value over the .range
        options.delay.range = args.delay
        options.delay.value = 0.0
        mutator = threading.Thread(target=delay_range, kwargs=options)
        mutator.daemon = True
        mutator.start()

    # Create all the specified tags/Attributes.  The enip_process function will (somehow) assign the
    # given tag name to reference the specified Attribute.
    for t in args.tags:
        tag_name, rest = t, ''
        if '=' in tag_name:
            tag_name, rest = tag_name.split('=', 1)
        tag_type, rest = rest or 'INT', ''
        tag_size = 1
        if '[' in tag_type:
            tag_type, rest = tag_type.split('[', 1)
            assert ']' in rest, "Invalid tag; mis-matched [...]"
            tag_size, rest = rest.split(']', 1)
        assert not rest, "Invalid tag specified; expected tag=<type>[<size>]: %r" % t
        tag_type = str(tag_type).upper()
        typenames = {
            "INT": parser.INT,
            "DINT": parser.DINT,
            "SINT": parser.SINT,
            "REAL": parser.REAL
        }
        assert tag_type in typenames, "Invalid tag type; must be one of %r" % list(
            typenames.keys())
        try:
            tag_size = int(tag_size)
        except:
            raise AssertionError("Invalid tag size: %r" % tag_size)

        # Ready to create the tag and its Attribute (and error code to return, if any).  If tag_size
        # is 1, it will be a scalar Attribute.
        log.normal("Creating tag: %s=%s[%d]", tag_name, tag_type, tag_size)
        tags[tag_name] = cpppo.dotdict()
        tags[tag_name].attribute = device.Attribute(
            tag_name,
            typenames[tag_type],
            default=(0 if tag_size == 1 else [0] * tag_size))
        tags[tag_name].error = 0x00

    # Use the Logix simulator by default (unless some other one was supplied as a keyword options to
    # main(), loaded above into 'options').  This key indexes an immutable value (not another dotdict
    # layer), so is not available for the web API to report/manipulate.
    options.setdefault('enip_process', logix.process)

    # The Web API

    # Deduce web interface:port address to bind, and correct types (default is address, above).
    # Default to the same interface as we're bound to, port 80.  We'll only start if non-empty --web
    # was provided, though (even if it's just ':', to get all defaults).  Usually you'll want to
    # specify at least --web :[<port>].
    http = args.web.split(':')
    assert 1 <= len(
        http) <= 2, "Invalid --web [<interface>]:[<port>}: %s" % args.web
    http = (str(http[0]) if http[0] else bind[0],
            int(http[1]) if len(http) > 1 and http[1] else 80)

    if args.web:
        assert 'web' in sys.modules, "Failed to import web API module; --web option not available.  Run 'pip install web.py'"
        logging.normal("EtherNet/IP Simulator Web API Server: %r" % (http, ))
        webserver = threading.Thread(target=web_api, kwargs={'http': http})
        webserver.daemon = True
        webserver.start()

    # The EtherNet/IP Simulator.  Pass all the top-level options keys/values as keywords, and pass
    # the entire tags dotdict as a tags=... keyword.  The server_main server.control signals (.done,
    # .disable) are also passed as the server= keyword.  We are using an cpppo.apidict with a long
    # timeout; this will block the web API for several seconds to allow all threads to respond to
    # the signals delivered via the web API.
    logging.normal("EtherNet/IP Simulator: %r" % (bind, ))
    kwargs = dict(options, latency=latency, tags=tags, server=srv_ctl)

    tf = network.server_thread
    tf_kwds = dict()
    if args.profile:
        tf = network.server_thread_profiling
        tf_kwds['filename'] = args.profile

    disabled = False  # Recognize toggling between en/disabled
    while not srv_ctl.control.done:
        if not srv_ctl.control.disable:
            if disabled:
                logging.detail("EtherNet/IP Server enabled")
                disabled = False
            network.server_main(address=bind,
                                target=enip_srv,
                                kwargs=kwargs,
                                idle_service=idle_service,
                                thread_factory=tf,
                                **tf_kwds)
        else:
            if not disabled:
                logging.detail("EtherNet/IP Server disabled")
                disabled = True
            time.sleep(latency)  # Still disabled; wait a bit

    return 0
Exemple #6
0
def main():
    return network.server_main(address=address, target=echo_server)
Exemple #7
0
def main():
    logging.basicConfig( **cpppo.log_cfg )
    return network.server_main( address=address, target=tnet_server )
Exemple #8
0
def main():
    logging.basicConfig(**cpppo.log_cfg)
    return network.server_main(address=address, target=tnet_server)
Exemple #9
0
def main():
    return network.server_main( address=address, target=echo_server )