Пример #1
0
    def make_nextdb(self, dbname=None):
        """create a new pvarch database, 
        copying pvs to save from an old database 
        (and that are in the current cache)
        """
        olddb  = SimpleDB(user=dbuser, passwd=dbpass,dbname=self.arch_db, host=dbhost,debug=0)
        olddb.use(self.arch_db)
        old_data = olddb.tables['pv'].select()

        cache_pvnames = self.get_pvnames()

       
        dbname = nextname(current=self.arch_db,dbname=dbname)
        self.create_emptydb(dbname)
        
        newdb = SimpleDB(user=dbuser, passwd=dbpass,dbname=dbname, host=dbhost,debug=0)
        pvtable = newdb.tables['pv']
        sys.stdout.write('adding %i pvs to DB %s\n' % (len(old_data),dbname))

        for p in old_data:
            if p['active'] == 'no' or p['name'] not in cache_pvnames:
                continue
            pvtable.insert(name=p['name'],
                           type=p['type'],
                           description=p['description'],
                           data_table=p['data_table'],
                           deadtime=p['deadtime'],
                           graph_type=p['graph_type'],
                           graph_lo=p['graph_lo'],
                           graph_hi=p['graph_hi'])

        self.__setrun(dbname)
        olddb.close()
        newdb.close()        
        return dbname
Пример #2
0
class MasterDB:
    """ general interface to Master Database of Epics Archiver.
    This is used by both by itself, and is sublcassed by
    Cache, ArchiveMaster, Instruments, and Alerts.

    Because of the multiple uses and especially because it is used
    as the sole interface by some Web processes, the API here may
    seem a bit disjointed, with a partial interface for Pairs and
    Alerts, but this allows many processes to have exactly one
    database connection.    
    """
    runs_title= '|  database         |       date range          | duration (days)|'
    runs_line = '|-------------------|---------------------------|----------------|'

    def_alert_msg ="""Hello,
  An alarm labeled  %LABEL%
  was detected for PV = '%PV%'
  The current value = %VALUE%.
  This is %COMP% the trip point value of %TRIP%
  """

    optokens = ('ne', 'eq', 'le', 'lt', 'ge', 'gt')
    opstrings= ('not equal to', 'equal to',
                'less than or equal to',    'less than',
                'greater than or equal to', 'greater than')
    
    ops = {'eq':'__eq__', 'ne':'__ne__', 
           'le':'__le__', 'lt':'__lt__', 
           'ge':'__ge__', 'gt':'__gt__'}
           
    def __init__(self,dbconn=None, **kw):

        self.db = SimpleDB(dbconn=dbconn)
        self.dbconn = self.db.conn
        self.db.use(master_db)
        self.db.get_cursor()
        self.db.read_table_info()

        self.info    = self.db.tables['info']
        self.cache   = self.db.tables['cache']
        self.runs    = self.db.tables['runs']
        self.pairs   = self.db.tables['pairs']
        self.stations  = self.db.tables['stations']
        self.instruments = self.db.tables['instruments']
        self.inst_pvs  = self.db.tables['instrument_pvs']
        self.alerts = self.db.tables['alerts']
        
        self.arch_db = self._get_info('db',  process='archive')
        self.pvnames = []
        
    def use_master(self):
        "point db cursor to use master database"
        self.db.use(master_db)

    def use_current_archive(self):
        "point db cursor to use current archive database"
        self.arch_db = self._get_info('db',  process='archive')        
        self.db.use(self.arch_db)

    def save_db(self,dbname=None):
        if dbname is None: dbname = self.arch_db
        sys.stdout.write('saving %s\n' % dbname)
        self.db.use(dbname)
        self.db.safe_dump(compress=True)
        self.db.use(master_db)
        
    def get_pvnames(self):
        """ generate self.pvnames: a list of pvnames in the cache"""
        self.pvnames = [i['pvname'] for i in self.cache.select()]
        # 
        #         for i in self.cache.select():
        #             if i['pvname'] not in self.pvnames:
        #                 self.pvnames.append(i['pvname'])
        return self.pvnames
    
    def request_pv_cache(self,pvname):
        """request a PV to be included in caching.
        will take effect once a 'process_requests' is executed."""
        self.db.use(master_db)
        npv = normalize_pvname(pvname)
        if len(self.pvnames)== 0: self.get_pvnames()
        if npv in self.pvnames: return

        cmd = "insert into requests (pvname,action,ts) values ('%s','add',%f)" % (npv,time.time())
        self.db.execute(cmd)
        print 'REQUEST_PV_CACHE: add ', pvname
        
    def add_pv(self,pvname,set_motor_pairs=True):
        """adds a PV to the cache: actually requests the addition, which will
        be handled by the next process_requests in mainloop().

        Here, we check for 'Motor' PV typs and make sure all motor fields are
        requested together, and that the motor fields are 'related' by assigning
        a pair_score = 10.                
        """

        pvname = normalize_pvname(pvname.strip())
        fields = (pvname,)
        if not valid_pvname(pvname):
            sys.stdout.write("## MasterDB add_pv invalid pvname = '%s'" % pvname)
            return fields
        
        prefix = pvname
        isMotor = False
        if pvname.endswith('.VAL'):
            prefix = pvname[:-4]

        p = epics.PV(pvname)
        p.wait_for_connection(timeout=0.1)
        print 'Master Add PVname = ', pvname, p.connected
        if p.connected: 
            self.request_pv_cache(pvname)
            if ('.' not in prefix and p.type == 'double'):
                rtype = epics.PV(prefix+'.RTYP')
                rtype.wait_for_connection(0.1)
                if rtype is not None:
                    isMotor = 'motor' == rtype.get()
            if isMotor:
                fields = tuple(["%s%s" % (prefix,i) for i in motor_fields])
                pvs = []
                for pvname in fields:
                    pvs.append(epics.PV(pvname))

                epics.poll()
                for p in pvs:
                    p.wait_for_connection(timeout=0.5)
                    if p.connected:
                        self.request_pv_cache(p.pvname)
                    
        if isMotor and set_motor_pairs:
            time.sleep(0.25)
            self.set_allpairs(fields)
            
        return fields
            

    def drop_pv(self,pvname):
        """drop a PV from the caching process -- really this 'suspends updates'
        will take effect once a 'process_requests' is executed."""
        npv = normalize_pvname(pvname)
        if len(self.pvnames)== 0: self.get_pvnames()
        if not npv in self.pvnames: return

        cmd = "insert into requests (pvname,action) values ('%s','suspend')" % npv
        self.db.execute(cmd)

    def get_recent(self,dt=60):
        """get recent additions to the cache, those
        inserted in the last  dt  seconds."""
        where = "ts>%f order by ts" % (time.time() - dt)
        return self.cache.select(where=where)
        
    def dbs_for_time(self, t0=SEC_DAY, t1=MAX_EPOCH):
        """ return list of databases with data in the given time range"""
        timerange = ( min(t0,t1) - SEC_DAY, max(t0,t1) + SEC_DAY)
        where = "stop_time>=%i and start_time<=%i order by start_time"
        r = []
        for i in self.runs.select(where=where % timerange):
            if i['db'] not in r: r.append(i['db'])
        return r

    def close(self):
        "close db connection"
        self.db.close()
    
    def _get_info(self,name='db',process='archive'):
        " get value from info table"
        try:
            return self.info.select_one(where="process='%s'" % process)[name]
        except:
            return None

    def _set_info(self,process='archive',**kw):
        " set value(s) in the info table"
        # print 'SET INFO ', process, kw
        self.info.update("process='%s'" % process, **kw)

    def get_cache_status(self):
        " get status of caching process"
        return self._get_info('status', process='cache')

    def get_arch_status(self):
        " get status of archiving process"
        return self._get_info('status', process='archive')

    def set_cache_status(self,status):
        " set status of caching process"                
        return self._set_info(status=status,  process='cache')

    def set_arch_status(self,status):
        " set status of archiving process"        
        return self._set_info(status=status,  process='archive')

    def get_cache_pid(self):
        " get pid of caching process"
        return self._get_info('pid',    process='cache')

    def get_arch_pid(self):
        " get pid of archiving process"        
        return self._get_info('pid',    process='archive')

    def set_cache_pid(self,pid):
        " set pid of caching process"
        return self._set_info(pid=pid,  process='cache')

    def set_arch_pid(self,pid):
        " set pid of archiving process"        
        return self._set_info(pid=pid,  process='archive')

    ##
    ## Status/Activity Reports 
    def get_npvs(self):
        """return the number of pvs in archive"""
        self.db.use(self.arch_db)
        r = self.db.exec_fetch('select id from pv')
        self.db.use(master_db)
        return len(r)

    def arch_nchanged(self,minutes=10,max = None):
        """return the number of values archived in the past minutes. """
        self.db.use(self.arch_db)
        n = 0
        t0 = time.time()
        dt = (time.time()-minutes*60.)
        q = "select pv_id from pvdat%3.3i where time > %f " 
        self.db.get_cursor()
        for i in range(1,129):
            n = n + self.db.cursor.execute(q % (i,dt))
            if max is not None and n > max: break
        self.db.use(master_db)
        return n

    def arch_report(self,minutes=10):
        """return a report (list of text lines) for archiving process, """
        npvs     = self.get_npvs()
        o = ["Current Database=%s, status=%s, PID=%i, %i PVs in archive" %(self.arch_db,
                                                                           self.get_arch_status(),
                                                                           self.get_arch_pid(),npvs)]
        return o

    def arch_full_report(self,minutes=10):
        """return a report (list of text lines) for archiving process,
        giving the number of values archived in the past minutes.
        """
        nchanged = self.arch_nchanged(minutes=minutes)
        npvs     = self.get_npvs()
        
        o = ["Current Database=%s, status=%s, PID=%i, %i PVs in archive" %(self.arch_db, 
                                                                 self.get_arch_status(),
                                                                 self.get_arch_pid(),npvs),
             "%i values archived in past %i minutes" % (nchanged, minutes)]
        return o

    def cache_report(self,brief=False,dt=60):
        """return a report (list of text lines) for caching process,
        giving number of values cached in the past dt seconds.
        Use 'brief=False' to show which PVs have been cached.
        """
        out = []
        pid = self.get_cache_pid()
        ret = self.cache.select(where="ts> %i order by ts" % (time.time()-dt))
        fmt = "  %s %.25s = %s"
        if not brief:
            for r in ret:
                out.append(fmt % (tformat(t=r['ts'],format="%H:%M:%S"),
                                  r['pvname']+' '*20, r['value']) )
                    
        fmt = '%i PVs had values updated in the past %i seconds. pid=%i'
        out.append(fmt % (len(ret),dt,pid))
        return out
        
    def runs_report(self, n=10):
        """return a report (list of text lines) for the archiving runs
        showing the time ranges for the (at most) 10 most recent runs.
        """
        r = []
        for i in self.runs.select(where='1=1 order by start_time desc limit %i' % n):
            timefmt = "%6.2f "
            if  i['db'] == self.arch_db:
                timefmt = "%6.1f*"
                i['stop_time'] = time.time()
            days = timefmt % ((i['stop_time'] - i['start_time'])/(24*3600.0))
            drange = "%s to %s" %(tformat(i['start_time'],format="%Y-%m-%d"),
                                  tformat(i['stop_time'],format="%Y-%m-%d"))
            
            r.append("| %16s  | %24s  |   %10s   |" % (i['db'],drange,days))
        r.reverse()
        out = [self.runs_line,self.runs_title,self.runs_line]
        for i in r: out.append(i)
        out.append(self.runs_line)
        return out

    ##
    ## Pairs
    def get_related_pvs(self,pv,minscore=1):
        """return a list of related pvs to the provided pv
        with a minumum pair score"""
       
        out = []
        tmp = []
        npv = normalize_pvname(pv)
        if len(self.pvnames)== 0: self.get_pvnames()
        if npv not in self.pvnames: return out
        for i in ('pv1','pv2'):
            where = "%s='%s' and score>=%i order by score" 
            for j in self.pairs.select(where = where % (i,npv,minscore)):
                tmp.append((j['score'],j['pv1'],j['pv2']))
        tmp.sort()
        for r in tmp:
            if   r[1] == npv:  out.append(r[2])
            elif r[2] == npv:  out.append(r[1])
        out.reverse()
        return out

    def __get_pvpairs(self,pv1,pv2):
        "fix and sort 2 pvs for use in the pairs tables"
        p = [normalize_pvname(pv1),normalize_pvname(pv2)]
        p.sort()
        return tuple(p)
    
        
    def get_pair_score(self,pv1,pv2):
        "set pair score for 2 pvs"        
        p = self.__get_pvpairs(pv1,pv2)
        score = -1
        if len(self.pvnames)== 0: self.get_pvnames()
        if (p[0] in self.pvnames) and (p[1] in self.pvnames):
            o  = self.pairs.select_one(where= "pv1='%s' and pv2='%s'" % p)
            score = int(o.get('score',-1))
        return score

    def set_pair_score(self,pv1,pv2,score=None):
        "set pair score for 2 pvs"
        p = self.__get_pvpairs(pv1,pv2)
        
        current_score  = self.get_pair_score(p[0],p[1])
        if current_score is None:
            wait_count = 0
            while current_score is None and wait_count < 10:
                time.sleep(0.1)
                self.get_pvnames()

                current_score  = self.get_pair_score(p[0],p[1])                
                wait_count = wait_count + 1
                
        # a current_score=None means the pv pairs may not be known yet.
        if current_score is None:
            current_score = -1
            score  = 1
            
        if score is None: score = 1 + current_score
       
        if current_score <= 0:
            q = "insert into pairs set score=%i, pv1='%s', pv2='%s'"
        else:
            q = "update pairs set score=%i where pv1='%s' and pv2='%s'"
        self.db.exec_fetch(q % (score,p[0],p[1]))
        
    def increment_pair_score(self,pv1,pv2):
        """increase by 1 the pair score for two pvs """
        self.set_pair_score(pv1,pv2,score=None)

    def set_allpairs(self,pvlist,score=10):
        """for a list/tuple of pvs, set all pair scores
        to be at least the provided score"""
        if not isinstance(pvlist,(tuple,list)): return
        _tmp = list(pvlist[:])

        # these may be newly added names, so may not yet be
        # in pvnames.  If not, let's give them a chance!
        newnames = False
        wait_count = 0
        if len(self.pvnames)== 0: self.get_pvnames()
        while newnames and wait_count < 10:
            newnames = False
            for i in _tmp:
                newnames = newnames or (i not in self.pvnames)

            time.sleep(0.1)
            self.get_pvnames()
            wait_count = wait_count + 1

        while _tmp:
            a = _tmp.pop()
            for b in _tmp:
                if self.get_pair_score(a,b)<score:
                    self.set_pair_score(a,b,score=score)
                    
        self.pairs.select()

    ##
    ## Instruments
    def get_instruments_with_pv(self,pv):
        """ return a list of (instrument ids, inst name, station name) for
        instruments which  contain a named pv"""
        inames = []
        pvn = normalize_pvname(pv)
        for r in self.inst_pvs.select(where="pvname='%s'" % pvn):
            inst = self.instruments.select_one(where="id=%i" % r['inst'])
            sta  = self.stations.select_one(where="id=%i" % inst['station'])
            inames.append((r['inst'],inst['name'],sta['name']))
        return inames

    ##
    ## Alerts
    def get_alerts(self,pvname=None,name=None):
        """ return a list of alerts for a pvname"""
        where = '1=1'
        if pvname is not None: 
            pvname = normalize_pvname(pvname)
            where = "pvname='%s'" % pvname
        
        if name is not None:
            where = "%s and name='%s'" % (where,clean_input(name))

        return self.alerts.select(where=where)

    def get_alert_with_id(self,id):
        """ return a list of alerts for a pvname"""
        return self.alerts.select_one(where="id=%i" % id)

    ## Alerts
    def remove_alert(self, id=None):
        """remove an alert"""
        if id is None: return
        q = "delete from alerts where id=%i" % int(id)
        self.db.execute(q)

   
    def add_alert(self, pvname=None,name=None,
                  mailto=None,  mailmsg=None, timeout=30,
                  compare='ne', trippoint=None, **kw):
        """add  a new alert"""
        if pvname is None: return
        
        pvname = normalize_pvname(pvname)        
        if name is None: name = pvname
        if len(self.pvnames)== 0: self.get_pvnames()
        if pvname not in self.pvnames:
            self.add_pv(pvname)

        active = 'yes'
        if mailto  is None:    active,mailto = ('no','')
        if mailmsg  is None:   active,mailmsg= ('no','')
        if trippoint is None:  active,trippoint = ('no',0.)

        if compare not in self.optokens: compare = 'ne'
        
        self.alerts.insert(name=name,pvname=pvname,active=active,
                           mailto=mailto,mailmsg=mailmsg, timeout=30,
                           compare=compare, trippoint=trippoint)
                  
        for a in self.get_alerts(pvname=pvname,name=name):
            val = epics.caget(pvname)
            self.check_alert(a['id'],val)
        
    def update_alert(self,id=None,**kw):
        """modify an existing alert, index with id, and passing in
        keyword args for ('pvname','name','mailto','mailmsg',
        'trippoint','compare','status','active')
        """
        if id is None: return
        where = "id=%i"% id        
        mykw = {}
        for k,v in kw.items():
            if k in ('pvname','name','mailto','mailmsg','timeout',
                     'trippoint','compare','status','active'):
                maxlen = 1024
                if k == 'mailmsg': maxlen = 32768
                v = clean_input(v,maxlen=maxlen)
                if 'compare' == k:
                    if not v in self.optokens:  v = 'ne'
                elif 'status' == k:
                    if v != 'ok': v = 'alarm'
                elif 'active' == k:
                    if v != 'no': v = 'yes'
                mykw[k]=v
        self.alerts.update(where=where,**mykw)
        a   = self.get_alert_with_id(id)
        val = epics.caget(a['pvname'])
        self.check_alert(id,val)

    def check_alert(self,id,value,sendmail=False):
        """
        check alert status, sending mail if necessary.
        returns two boolean values: (Value_OK, Mail_Was_Sent)
        """
        # print '## check_alert with sendmail=True', id, value
        where = "id=%i"% id
        alarm = self.alerts.select_one(where=where)
        
        # if alarm is not active, return now
        if 'no' == alarm['active']: return True,False

        # coerce values to strings or floats for comparisons
        convert = str
        if isinstance(value,(int,long,float,complex)):  convert = float

        value     = convert(value)
        trippoint = convert(alarm['trippoint'])
        cmp       = self.ops[alarm['compare']]
        
        # compute new alarm status: note form  'value.__ne__(trippoint)'
        value_ok = not getattr(value,cmp)(trippoint)

        old_value_ok = (alarm['status'] == 'ok')
        notify = sendmail and old_value_ok and (not value_ok)
        if old_value_ok != value_ok:
            # update the status filed in the alerts table
            status = 'alarm'
            if value_ok: status = 'ok'
            self.alerts.update(status=status,where=where)

            # send mail if value is now not ok!
            if notify:
                self.sendmail(alarm,value)

        # if sendmail: print '##check_alert: ' , value_ok, notify
        return value_ok, notify
            
    
    def sendmail(self,alarm,value):
        """ send an alert email from an alarm dict holding
        the appropriate row of the alert table.        
        """
        mailto = alarm['mailto']
        pvname = alarm['pvname']
        label  = alarm['name']
        compare= alarm['compare']
        msg    = alarm['mailmsg']
        
        if mailto in ('', None) or pvname in ('', None): return

        mailto = mailto.replace('\r','').replace('\n','')

        trippoint = str(alarm['trippoint'])
        mailto    = tuple(mailto.split(','))
        subject   = "[Epics Alert] %s" % (label)

        if msg in ('', None):  msg = self.def_alert_msg
        
        msg  = clean_mail_message(msg)

        opstr = 'not equal to'
        for tok,desc in zip(self.optokens, self.opstrings):
            if tok == compare: opstr = desc

        # fill in 'template' values in mail message
        for k,v in {'PV': pvname,  'LABEL':label,
                    'COMP': opstr, 'VALUE': str(value),  
                    'TRIP': str(trippoint)}.items():
            msg = msg.replace("%%%s%%" % k, v)

        # do %PV(XX)% replacements
        mlines = msg.split('\n')
        for i,line in enumerate(mlines):
            nmatch = 0
            match = re_showpv(line)
            while match is not None and nmatch<25:
                try:
                    pvn = match.groups()[0]
                    rep = "%(cvalue)s" % self.cache.select_one(where="pvname='%s'" % pvn)
                    line = line.replace('%%PV(%s)%%' % pvn,rep)
                except:
                    line = line.replace('%%PV(%s)%%' % pvn, 'Unknown_PV(%s)' % pvn)
                match = re_showpv(line)
                nmatch = nmatch + 1
            mlines[i] = line
        msg = "From: %s\r\nSubject: %s\r\n%s\nSee %s/plot/%s\n" % \
              (mailfrom,subject,'\n'.join(mlines),cgi_url,pvname)

        try:
            s = smtplib.SMTP(mailserver)
            s.sendmail(mailfrom,mailto,msg)
            s.quit()
        except:
            sys.stdout.write("Could not send Alert mail:  mail not configured??")