示例#1
0
 def __init__(self):
     self.__queue = Queue()
     self.__lock = Lock()
     # Mapping of LID to deque (so can serialise messages for same LID). Appended to right, removed from left.
     self.__lid_mapping = {}
     self.__local = thread_local()
     self.__new_msg = Event()
示例#2
0
 def __init__(self, name, num_workers=1, iotclient=None, daemonic=False):
     self.__name = name
     self.__num_workers = num_workers
     self.__iotclient = iotclient
     self.__daemonic = daemonic
     #
     self.__queue = LidSerialisedQueue()
     self.__stop = Event()
     self.__stop.set()
     self.__threads = []
     self.__cache = {}
示例#3
0
    def __init__(self, fname, iotclient, num_workers):
        self.__fname = fname
        self.__name = self.__fname_to_name(fname)
        self.__workers = ThreadPool(self.__name,
                                    num_workers=num_workers,
                                    iotclient=iotclient)
        self.__thread = Thread(target=self.__run,
                               name=('stash-%s' % self.__name))
        self.__stop = Event()

        self.__stash = None
        self.__stash_lock = RLock()
        self.__stash_hash = None

        self.__pname = splitext(self.__fname)[0] + '_props.json'
        self.__properties = None
        self.__properties_changed = False

        self.__load()
示例#4
0
def main():  # pylint: disable=too-many-return-statements,too-many-branches
    if len(argv) < 2:
        if not exists(argv[1]):
            return usage()
    try:
        cfg = Config(argv[1])
    except:
        logger.exception("Failed to load/parse Config file '%s'. Giving up.",
                         argv[1])
        return 1

    wwwpath = cfg.get(EXTMON2, 'wwwpath')
    if wwwpath is None:
        logger.error(
            "Config file must have [extmon2] section with wwwpath = /path/to/storage"
        )
        return 1
    wwwpath = abspath(wwwpath)
    if not exists(wwwpath):
        mkdir(wwwpath)
    elif exists(wwwpath) and not isdir(wwwpath):
        logger.error("Config file must have [extmon2] wwpath not a directory")
        return 1

    template = cfg.get(EXTMON2, 'template')
    if template is None or not exists(template):
        logger.error(
            "Config file must have [extmon2] section with template = /path/to/file.html"
        )
        return 1

    agent = cfg.get(EXTMON2, 'agent')
    if agent is None or not exists(agent):
        logger.error(
            "Config file must have [extmon2] section with agent = /path/to/agent.ini"
        )
        return 1

    feeds_list = cfg.get(EXTMON2, 'feeds')
    if feeds_list is None:
        logger.error(
            "Config file must have [extmon2] feeds = \n\nFeedeName\n\tFeed_Two"
        )
        return 1

    for feed in feeds_list:
        if cfg.get(feed, 'guid') is None or cfg.get(feed, MAX_AGE) is None:
            logger.error(
                "Config section for feed [%s] must have guid and max_age",
                feed)
            return 1

    stop_evt = Event()
    thread = Thread(target=extmon, name='extmon', args=(
        cfg,
        stop_evt,
    ))
    thread.start()

    if 'IOTIC_BACKGROUND' in environ:
        from signal import signal, SIGINT, SIGTERM

        logger.info("Started in non-interactive mode.")

        def exit_handler(signum, frame):  # pylint: disable=unused-argument
            logger.info('Shutdown requested')
            stop_evt.set()

        signal(SIGINT, exit_handler)
        signal(SIGTERM, exit_handler)

        while not stop_evt.is_set():
            stop_evt.wait(timeout=5)
        stop_evt.set()
    else:
        try:
            while not stop_evt.is_set():
                logger.info('Enter ctrl+c to exit')
                stop_evt.wait(timeout=600)
        except SystemExit:
            pass
        except KeyboardInterrupt:
            pass
        stop_evt.set()

    logger.info("Waiting for thread to finish...")
    thread.join()
    return 0
示例#5
0
class LidSerialisedQueue(object):
    """Thread-safe queue which ensures enqueued Messages for the same lid are not handled by multiple threads at the
    same time."""
    def __init__(self):
        self.__queue = Queue()
        self.__lock = Lock()
        # Mapping of LID to deque (so can serialise messages for same LID). Appended to right, removed from left.
        self.__lid_mapping = {}
        self.__local = thread_local()
        self.__new_msg = Event()

    def thread_init(self):
        """Must be called in each thread which is to use this instance, before using get()!"""
        # LID which is being processed by this thread, if any
        self.__local.own_lid = None

    @property
    def empty(self):
        return not self.__lid_mapping and self.__queue.empty()

    def put(self, qmsg):
        if not isinstance(qmsg, Message):
            raise ValueError
        self.__queue.put(qmsg)
        self.__new_msg.set()

    def __get_for_current_lid(self):
        """Returns next message for same LID as previous message, if available. None otherwise. MUST be called within
        lock!
        """
        local = self.__local
        try:
            return self.__lid_mapping[local.own_lid].popleft()
        # No messages left for this LID (deque.popleft), remove LID association
        except IndexError:
            del self.__lid_mapping[local.own_lid]
            local.own_lid = None
        # No LIDs being processed (mapping)
        except KeyError:
            pass

        return None

    def get(self, timeout=None):
        """Raises queue.Empty exception if no messages are available after timeout"""
        with self.__lock:
            msg = self.__get_for_current_lid()

            if not msg:
                queue = self.__queue
                lid_mapping = self.__lid_mapping

                while True:
                    # Instead of blocking on get(), release lock so other threads have a chance to request existing lid
                    # messages (above, via __get_for_current_lid).
                    if not queue.qsize() and timeout:
                        self.__new_msg.clear()
                        try:
                            self.__lock.release()
                            self.__new_msg.wait(timeout)
                        finally:
                            self.__lock.acquire()
                    msg = queue.get_nowait()
                    # Enqueue message with LID already being dealt with in LID-specific queue, otherwise can process
                    # oneself.
                    if msg.lid in lid_mapping:
                        lid_mapping[msg.lid].append(msg)
                    else:
                        # Currently nobody else is processing messages with this LID, so can use oneself
                        self.__local.own_lid = msg.lid
                        lid_mapping[msg.lid] = deque()
                        break

        return msg
示例#6
0
class ThreadPool(object):  # pylint: disable=too-many-instance-attributes

    __share_time_fmt = '%Y-%m-%dT%H:%M:%S.%fZ'

    def __init__(self, name, num_workers=1, iotclient=None, daemonic=False):
        self.__name = name
        self.__num_workers = num_workers
        self.__iotclient = iotclient
        self.__daemonic = daemonic
        #
        self.__queue = LidSerialisedQueue()
        self.__stop = Event()
        self.__stop.set()
        self.__threads = []
        self.__cache = {}

    def start(self):
        if self.__stop.is_set():
            self.__stop.clear()
            for i in range(0, self.__num_workers):
                thread = Thread(target=self.__worker,
                                name=('tp-%s-%d' % (self.__name, i)))
                thread.daemon = self.__daemonic
                self.__threads.append(thread)
            for thread in self.__threads:
                thread.start()

    def submit(self, lid, idx, diff, complete_cb=None):
        self.__queue.put(Message(lid, idx, diff, complete_cb))

    def stop(self):
        if not self.__stop.is_set():
            self.__stop.set()
            for thread in self.__threads:
                thread.join()
            del self.__threads[:]

    @property
    def queue_empty(self):
        return self.__queue.empty

    def __worker(self):
        logger.debug("Starting")
        self.__queue.thread_init()
        stop_is_set = self.__stop.is_set
        queue_get = self.__queue.get
        handle_thing_changes = self.__handle_thing_changes

        while not stop_is_set():
            try:
                qmsg = queue_get(timeout=.25)
            except Empty:
                continue  # queue.get timeout ignore

            while True:
                try:
                    handle_thing_changes(qmsg.lid, qmsg.diff)
                except LinkException:
                    logger.warning("Network error, will retry lid '%s'",
                                   qmsg.lid)
                    self.__stop.wait(timeout=1)
                    continue
                except IOTAccessDenied:
                    logger.critical(
                        "IOTAccessDenied - Local limit exceeded - Aborting")
                    kill(getpid(), SIGUSR1)
                    return
                except:
                    logger.error(
                        "Failed to process thing changes (Uncaught exception)  - Aborting",
                        exc_info=DEBUG_ENABLED)
                    kill(getpid(), SIGUSR1)
                    return
                break

            logger.debug("completed thing %s", qmsg.lid)
            if qmsg.complete_cb:
                try:
                    qmsg.complete_cb(qmsg.lid, qmsg.idx)
                except:
                    logger.error("complete_cb failed for %s",
                                 qmsg.lid,
                                 exc_info=DEBUG_ENABLED)
                    kill(getpid(), SIGUSR1)
                    return

    @classmethod
    def __lang_convert(cls, lang):
        if lang == '':
            return None
        return lang

    def __handle_thing_changes(self, lid, diff):  # pylint: disable=too-many-branches
        if lid not in self.__cache:
            self.__cache[lid] = {
                THING: self.__iotclient.create_thing(lid),
                POINTS: {}
            }
        iotthing = self.__cache[lid][THING]

        if PUBLIC in diff and diff[PUBLIC] is False:
            iotthing.set_public(False)

        thingmeta = None
        for chg, val in diff.items():
            if chg == TAGS and len(val):
                iotthing.create_tag(val)
            elif chg == LABELS and val:
                if thingmeta is None:
                    thingmeta = iotthing.get_meta()
                for lang, label in val.items():
                    thingmeta.set_label(label, lang=self.__lang_convert(lang))
            elif chg == DESCRIPTIONS and val:
                if thingmeta is None:
                    thingmeta = iotthing.get_meta()
                for lang, description in val.items():
                    thingmeta.set_description(description,
                                              lang=self.__lang_convert(lang))
            elif chg == LOCATION and val[0] is not None:
                if thingmeta is None:
                    thingmeta = iotthing.get_meta()
                thingmeta.set_location(val[0], val[1])
        if thingmeta is not None:
            thingmeta.set()

        for pid, pdiff in diff[POINTS].items():
            self.__handle_point_changes(iotthing, lid, pid, pdiff)

        if PUBLIC in diff and diff[PUBLIC] is True:
            iotthing.set_public(True)

    def __handle_point_changes(self, iotthing, lid, pid, pdiff):  # pylint: disable=too-many-branches
        if pid not in self.__cache[lid][POINTS]:
            if pdiff[FOC] == R_FEED:
                iotpoint = iotthing.create_feed(pid)
            elif pdiff[FOC] == R_CONTROL:
                iotpoint = iotthing.create_control(pid)
            self.__cache[lid][POINTS][pid] = iotpoint
        iotpoint = self.__cache[lid][POINTS][pid]
        pointmeta = None

        for chg, val in pdiff.items():
            if chg == TAGS and len(val):
                iotpoint.create_tag(val)
            elif chg == RECENT:
                iotpoint.set_recent_config(max_samples=pdiff[RECENT])
            elif chg == LABELS and val:
                if pointmeta is None:
                    pointmeta = iotpoint.get_meta()
                for lang, label in val.items():
                    pointmeta.set_label(label, lang=self.__lang_convert(lang))
            elif chg == DESCRIPTIONS and val:
                if pointmeta is None:
                    pointmeta = iotpoint.get_meta()
                for lang, description in val.items():
                    pointmeta.set_description(description,
                                              lang=self.__lang_convert(lang))
        if pointmeta is not None:
            pointmeta.set()

        sharedata = {}
        for label, vdiff in pdiff[VALUES].items():
            if SHAREDATA in vdiff:
                sharedata[label] = vdiff[SHAREDATA]
            self.__handle_value_changes(lid, pid, label, vdiff)

        sharetime = None
        if SHARETIME in pdiff:
            sharetime = pdiff[SHARETIME]
            if isinstance(sharetime, string_types):
                try:
                    sharetime = datetime.strptime(sharetime,
                                                  self.__share_time_fmt)
                except:
                    logger.warning(
                        "Failed to make datetime from time string '%s' !Will use None!",
                        sharetime)
                    sharetime = None

        if len(sharedata):
            iotpoint.share(data=sharedata, time=sharetime)

        if SHAREDATA in pdiff:
            iotpoint.share(data=pdiff[SHAREDATA], time=sharetime)

    def __handle_value_changes(self, lid, pid, label, vdiff):
        """
        Note: remove & add values if changed, share data if data
        """
        iotpoint = self.__cache[lid][POINTS][pid]
        if VTYPE in vdiff and vdiff[VTYPE] is not None:
            iotpoint.create_value(label,
                                  vdiff[VTYPE],
                                  lang=vdiff[LANG],
                                  description=vdiff[DESCRIPTION],
                                  unit=vdiff[UNIT])
示例#7
0
class Stash(object):  # pylint: disable=too-many-instance-attributes
    @classmethod
    def __fname_to_name(cls, fname):
        return splitext(path_split(fname)[-1])[0]

    def __init__(self, fname, iotclient, num_workers):
        self.__fname = fname
        self.__name = self.__fname_to_name(fname)
        self.__workers = ThreadPool(self.__name,
                                    num_workers=num_workers,
                                    iotclient=iotclient)
        self.__thread = Thread(target=self.__run,
                               name=('stash-%s' % self.__name))
        self.__stop = Event()

        self.__stash = None
        self.__stash_lock = RLock()
        self.__stash_hash = None

        self.__pname = splitext(self.__fname)[0] + '_props.json'
        self.__properties = None
        self.__properties_changed = False

        self.__load()

    def start(self):
        self.__workers.start()
        self.__submit_diffs()
        self.__thread.start()

    def stop(self):
        if not self.__stop.is_set():
            self.__stop.set()
            self.__thread.join()
            self.__workers.stop()
            self.__save()

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, typ, value, traceback):
        self.stop()

    def is_alive(self):
        return self.__thread.is_alive()

    def __load(self):  # pylint: disable=too-many-branches
        fsplit = splitext(self.__fname)
        if fsplit[1] == '.json':
            if exists(self.__fname):
                # Migrate from json to ubjson
                with self.__stash_lock:
                    with open(self.__fname, 'r') as f:
                        self.__stash = json.loads(f.read())
                    rename(self.__fname, self.__fname + '.old')

        if fsplit[1] != '.ubjz':
            self.__fname = fsplit[0] + '.ubjz'

        if exists(self.__fname):
            with self.__stash_lock:
                with gzip_open(self.__fname, 'rb') as f:
                    self.__stash = ubjson.loadb(f.read())
        elif self.__stash is None:
            self.__stash = {
                THINGS: {},  # Current/last state of Things
                DIFF: {},  # Diffs not yet updated in Iotic Space
                DIFFCOUNT: 0
            }  # Diff counter

        if not exists(self.__pname):
            self.__properties = {}
        else:
            with self.__stash_lock:
                with open(self.__pname, 'r') as f:
                    self.__properties = json.loads(f.read())

        with self.__stash_lock:
            stash_copy = deepcopy(self.__stash)
            self.__stash = {}
            # Migrate built-in keys
            for key, value in stash_copy.items():
                if key in [THINGS, DIFF, DIFFCOUNT]:
                    logger.debug("--> Migrating built-in %s", key)
                    self.__stash[key] = stash_copy[key]
            # Migrate bad keys
            for key, value in stash_copy.items():
                if key not in [THINGS, DIFF, DIFFCOUNT]:
                    if key not in stash_copy[THINGS]:
                        logger.info("--> Migrating key to THINGS %s", key)
                        self.__stash[THINGS][key] = value
                        self.__stash.pop(key, None)
            # Remove redundant LAT/LONG (LOCATION used instead)
            for el, et in self.__stash[THINGS].items():  # pylint: disable=unused-variable
                et.pop(LAT, None)
                et.pop(LONG, None)

        self.__save()

    def __calc_stashdump(self):
        with self.__stash_lock:
            stashdump = ubjson.dumpb(self.__stash)
            m = md5()
            m.update(stashdump)
            stashhash = m.hexdigest()
            if self.__stash_hash != stashhash:
                self.__stash_hash = stashhash
                return stashdump
            return None

    def __save(self):
        stashdump = self.__calc_stashdump()
        if stashdump is not None:
            with gzip_open(self.__fname, 'wb') as f:
                f.write(stashdump)

        if len(self.__properties) and self.__properties_changed:
            with self.__stash_lock:
                with open(self.__pname, 'w') as f:
                    json.dump(self.__properties, f)

    def get_property(self, key):
        with self.__stash_lock:
            if not isinstance(key, string_types):
                raise ValueError("key must be string")
            if key in self.__properties:
                return self.__properties[key]
            return None

    def set_property(self, key, value=None):
        with self.__stash_lock:
            if not isinstance(key, string_types):
                raise ValueError("key must be string")
            if value is None and key in self.__properties:
                del self.__properties[key]
            if value is not None:
                if isinstance(value, string_types) or isinstance(
                        value, number_types):
                    if key not in self.__properties or self.__properties[
                            key] != value:
                        self.__properties_changed = True
                        self.__properties[key] = value
                else:
                    raise ValueError("value must be string or int")

    def __run(self):
        logger.info("Started.")
        while not self.__stop.is_set():
            self.__save()
            self.__stop.wait(timeout=SAVETIME)

    def create_thing(self, lid):
        if lid in self.__stash[THINGS]:
            thing = Thing(lid,
                          stash=self,
                          public=self.__stash[THINGS][lid][PUBLIC],
                          labels=self.__stash[THINGS][lid][LABELS],
                          descriptions=self.__stash[THINGS][lid][DESCRIPTIONS],
                          tags=self.__stash[THINGS][lid][TAGS],
                          points=self.__stash[THINGS][lid][POINTS],
                          lat=self.__stash[THINGS][lid][LOCATION][0],
                          long=self.__stash[THINGS][lid][LOCATION][1])
            return thing
        return Thing(lid, new=True, stash=self)

    def __calc_diff(self, thing):  # pylint: disable=too-many-branches
        if not len(thing.changes):
            changes = 0
            for pid, point in thing.points.items():
                changes += len(point.changes)
            if changes == 0:
                return None, None

        ret = 0
        diff = {}
        if thing.new:
            # Note: thing is new so no need to calculate diff.
            #  This shows the diff dict full layout
            diff = {
                LID: thing.lid,
                TAGS: thing.tags,
                LOCATION: thing.location,
                LABELS: thing.labels,
                DESCRIPTIONS: thing.descriptions,
                POINTS: {}
            }
            # Prevent public setting to always be performed for new things
            if PUBLIC in thing.changes:
                diff[PUBLIC] = thing.public

            for pid, point in thing.points.items():
                diff[POINTS][pid] = self.__calc_diff_point(point)

        else:
            diff[LID] = thing.lid
            diff[POINTS] = {}
            for change in thing.changes:
                if change == PUBLIC:
                    diff[PUBLIC] = thing.public
                elif change == TAGS:
                    diff[TAGS] = thing.tags
                elif change.startswith(LABEL):
                    if LABELS not in diff:
                        diff[LABELS] = {}
                    lang = change.replace(LABEL, '')
                    diff[LABELS][lang] = thing.labels[lang]
                elif change.startswith(DESCRIPTION):
                    if DESCRIPTIONS not in diff:
                        diff[DESCRIPTIONS] = {}
                    lang = change.replace(DESCRIPTION, '')
                    diff[DESCRIPTIONS][lang] = thing.descriptions[lang]
                elif change == LOCATION:
                    diff[LOCATION] = thing.location
            for pid, point in thing.points.items():
                diff[POINTS][pid] = self.__calc_diff_point(point)

        with self.__stash_lock:
            self.__stash[DIFF][str(self.__stash[DIFFCOUNT])] = diff
            ret = self.__stash[DIFFCOUNT]
            self.__stash[DIFFCOUNT] += 1
        return ret, diff

    def __calc_diff_point(self, point):
        ret = {PID: point.lid, FOC: point.foc, VALUES: {}}
        if point.new:
            ret.update({LABELS: {}, DESCRIPTIONS: {}, RECENT: 0, TAGS: []})
        for change in point.changes:
            if change == TAGS:
                ret[TAGS] = point.tags
            elif change.startswith(LABEL):
                if LABELS not in ret:
                    ret[LABELS] = {}
                lang = change.replace(LABEL, '')
                ret[LABELS][lang] = point.labels[lang]
            elif change.startswith(DESCRIPTION):
                if DESCRIPTIONS not in ret:
                    ret[DESCRIPTIONS] = {}
                lang = change.replace(DESCRIPTION, '')
                ret[DESCRIPTIONS][lang] = point.descriptions[lang]
            elif change == RECENT:
                ret[RECENT] = point.recent_config
            elif change == SHAREDATA:
                ret[SHAREDATA] = point.sharedata
            elif change == SHARETIME:
                ret[SHARETIME] = point.sharetime
            elif change.startswith(VALUE):
                label = change.replace(VALUE, '')
                ret[VALUES][label] = self.__calc_value(point.values[label])
            # applicable only if no value attributes have changed (share only)
            elif change.startswith(VALUESHARE):
                label = change.replace(VALUESHARE, '')
                if label not in ret[VALUES]:
                    ret[VALUES][label] = {}
                ret[VALUES][label].update(
                    {SHAREDATA: point.values[label].pop(SHAREDATA)})
        return ret

    @classmethod
    def __calc_value(cls, value):
        ret = {}
        if VTYPE in value:
            ret = {
                VTYPE: value[VTYPE],
                LANG: value[LANG],
                DESCRIPTION: value[DESCRIPTION],
                UNIT: value[UNIT]
            }
        if SHAREDATA in value:
            ret[SHAREDATA] = value[SHAREDATA]
        return ret

    def __submit_diffs(self):
        """On start resubmit any diffs in the stash
        """
        with self.__stash_lock:
            for idx, diff in self.__stash[DIFF].items():
                logger.info("Resubmitting diff for thing %s", diff[LID])
                self.__workers.submit(diff[LID], idx, diff, self.__complete_cb)

    def _finalise_thing(self, thing):
        with thing.lock:
            idx, diff = self.__calc_diff(thing)
            if idx is not None:
                self.__workers.submit(diff[LID], idx, diff, self.__complete_cb)
                thing.clear_changes()

    def __complete_cb(self, lid, idx):
        with self.__stash_lock:
            idx = str(idx)
            diff = self.__stash[DIFF][idx]
            try:
                thing = self.__stash[THINGS][lid]
            except KeyError:
                thing = self.__stash[THINGS][lid] = {
                    PUBLIC: False,
                    LABELS: {},
                    DESCRIPTIONS: {},
                    TAGS: [],
                    POINTS: {},
                    LOCATION: (None, None)
                }

            empty = {}
            # Updated later separately
            points = diff.pop(POINTS, empty)
            # Have to be merged since update only affects subset of all labels/descriptions
            for item in (LABELS, DESCRIPTIONS):
                thing[item].update(diff.pop(item, empty))
            # Rest should be OK to replace (public, tags, location)
            thing.update(diff)

            # Points
            for pid, pdiff in points.items():
                try:
                    point = thing[POINTS][pid]
                except KeyError:
                    thing[POINTS][pid] = point = {
                        PID: pid,
                        VALUES: {},
                        LABELS: {},
                        DESCRIPTIONS: {},
                        TAGS: []
                    }
                # Updated later separately
                values = pdiff.pop(VALUES, empty)
                # Remove sharedata, sharetime before applying to stash
                for item in (SHAREDATA, SHARETIME):
                    pdiff.pop(item, None)
                # Have to be merged since update only affects subset of all labels/descriptions
                for item in (LABELS, DESCRIPTIONS):
                    point[item].update(pdiff.pop(item, empty))
                # Rest should be OK to replace (tags, foc, recent, share time, data)
                point.update(pdiff)

                # Values
                for label, value in values.items():
                    # Don't remember value share data
                    value.pop(SHAREDATA, None)
                    try:
                        # Might only have data set so must merge
                        point[VALUES][label].update(value)
                    except KeyError:
                        point[VALUES][label] = value

            del self.__stash[DIFF][idx]

    @property
    def queue_empty(self):
        return self.__workers.queue_empty