source: n4d/trunk/fuentes/install-files/usr/share/n4d/python-plugins/VariablesManager.py @ 3547

Last change on this file since 3547 was 3547, checked in by hectorgh, 3 years ago

VariablesManager? changes. Read changelog. startup thread is launched after service is up

File size: 19.2 KB
Line 
1import json
2import os.path
3import os
4import time
5import xmlrpclib
6import socket
7import netifaces
8import re
9import importlib
10import sys
11import tarfile
12import threading
13import subprocess
14import time
15import string
16
17class VariablesManager:
18
19        VARIABLES_FILE="/var/lib/n4d/variables"
20        VARIABLES_DIR="/var/lib/n4d/variables-dir/"
21        LOCK_FILE="/tmp/.llxvarlock"
22        INBOX="/var/lib/n4d/variables-inbox/"
23        TRASH="/var/lib/n4d/variables-trash/"
24        CUSTOM_INSTALLATION_DIR="/usr/share/n4d/variablesmanager-funcs/"
25        LOG="/var/log/n4d/variables-manager"
26       
27        def __init__(self):
28               
29                self.instance_id="".join(random.sample(string.letters+string.digits, 50))
30                self.server_instance_id=None
31                self.variables={}
32                self.variables_ok=False
33                self.variables_clients={}
34                self.variables_triggers={}
35                t=threading.Thread(target=self.check_clients,args=())
36                t.daemon=True
37                t.start()
38               
39                if os.path.exists(VariablesManager.LOCK_FILE):
40                        os.remove(VariablesManager.LOCK_FILE)
41                       
42                       
43                if os.path.exists(VariablesManager.VARIABLES_FILE):
44                        self.variables_ok,ret=self.load_json(VariablesManager.VARIABLES_FILE)
45                        try:
46                                os.remove(VariablesManager.VARIABLES_FILE)
47                        except:
48                                pass
49                else:
50                        self.variables_ok,ret=self.load_json(None)
51                       
52                if self.variables_ok:
53                        #print "\nVARIABLES FILE"
54                        #print "=============================="
55                        #self.listvars()
56                        self.read_inbox(False)
57                        #print "\nAFTER INBOX"
58                        #print "=============================="
59                        #print self.listvars(True)
60                        self.empty_trash(False)
61                        #print "\nAFTER TRASH"
62                        #print "=============================="
63                        #print self.listvars(True)
64                        self.add_volatile_info()
65                        self.write_file()
66                else:
67                        print("[VariablesManager] Loading variables failed because: " + str(ret))
68
69               
70        #def __init__
71       
72       
73        def startup(self,options):
74
75                if "REMOTE_VARIABLES_SERVER" in self.variables:
76                        self.register_n4d_instance_to_server()
77                       
78        #def startup
79       
80       
81        def is_ip_in_range(self,ip,network):
82               
83                try:
84                        return netaddr.ip.IPAddress(ip) in netaddr.IPNetwork(network).iter_hosts()
85                except:
86                        return False
87                       
88        #def is_ip_in_range
89       
90
91        def get_net_size(self,netmask):
92               
93                netmask=netmask.split(".")
94                binary_str=""
95                for octet in netmask:
96                        binary_str += bin(int(octet))[2:].zfill(8)
97                       
98                return str(len(binary_str.rstrip('0')))
99               
100        #def get_net_size
101
102
103        def get_ip(self):
104               
105                for item in netifaces.interfaces():
106                        tmp=netifaces.ifaddresses(item)
107                        if tmp.has_key(netifaces.AF_INET):
108                                if tmp[netifaces.AF_INET][0].has_key("broadcast") and tmp[netifaces.AF_INET][0]["broadcast"]=="10.0.2.255":
109                                        return tmp[netifaces.AF_INET][0]["addr"]
110                return None
111               
112        #def get_ip
113       
114
115        def route_get_ip(self,ip):
116               
117                p=subprocess.Popen(["ip route get %s"%ip],shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()
118                if "dev" in p[0]:
119                        dev=p[0].split("dev ")[1].split(" ")[0]
120                else:
121                        dev=None
122                return dev
123               
124        #def route_get_ip
125               
126
127        def get_mac_from_device(self,dev):
128
129                for item in netifaces.interfaces():
130                       
131                        try:
132                                i=netifaces.ifaddresses(item)
133                                mac=i[17][0]["addr"]
134                                broadcast=i[2][0]["broadcast"]
135                                network=broadcast
136                                netmask=i[2][0]["netmask"]
137                                network+="/%s"%self.get_net_size(netmask)
138                                ip=i[2][0]["addr"]
139                        except Exception as e:
140                                continue
141                       
142                        if dev=="lo":
143                                return mac
144                       
145                        if item==dev:
146                                return mac
147                               
148                return None
149
150        #def get_mac_from_device_in_server_network
151       
152       
153        def register_instance(self,autocompleted_secured_ip,mac):
154
155                client={}
156                client["last_check"]=int(time.time())
157                client["missed_pings"]=0
158                client["ip"]=autocompleted_secured_ip
159                self.variables_clients[mac]=client
160               
161                return self.instance_id
162
163        #def register_instance
164       
165
166        def register_n4d_instance_to_server(self):
167               
168                try:
169                        server_ip=socket.gethostbyname(self.variables["REMOTE_VARIABLES_SERVER"][u"value"])
170                        if self.get_ip()!=server_ip:
171                       
172                                c=xmlrpclib.ServerProxy("https://%s:9779"%server_ip)
173                                mac=self.get_mac_from_device(self.route_get_ip(server_ip))
174                                self.server_instance_id=c.register_instance("","VariablesManager","",mac)
175                       
176                except Exception as e:
177
178                        return None
179
180        #def register_n4d_instance_to_server   
181       
182       
183        def check_clients(self):
184               
185                while True:
186                       
187                        for item in self.variables_clients:
188                                ip=self.variables_clients[item]["ip"]
189                                sys.stdout.write("[VariablesManager] Checking client { MAC:%s IP:%s } ... "%(item,ip))
190                                c=xmlrpclib.ServerProxy("https://%s:9779"%ip)
191                                try:
192                                        c.get_methods()
193                                        self.variables_clients[item]["last_check"]=time.time()
194                                        self.variables_clients[item]["missed_pings"]=0
195                                        print("OK")
196                                except:
197                                        self.variables_clients[item]["missed_pings"]+=1
198                                        print("FAILED")
199                                        if self.variables_clients[item]["missed_pings"] >=3:
200                                                print "[VariablesManager] Removing client due to too many missed pings."
201                                                self.variables_clients.pop(item)
202                       
203                        time.sleep(60*5)
204               
205               
206        #def check_clients
207       
208        def notify_changes(self,variable):
209               
210                print "[VariablesManager] Notifying changes... "
211               
212                for mac in self.variables_clients:
213                       
214                        ip=self.variables_clients[mac]["ip"]
215                        c=xmlrpclib.ServerProxy("https://%s:9779"%ip)
216                        try:
217                                c.server_changed("","VariablesManager","",self.instance_id,variable)
218                               
219                        except:
220                                self.variables_clients[mac]["missed_pings"]+=1
221                               
222                        self.variables_clients[mac]["last_check"]=time.time()
223               
224        #def announce_changes
225       
226       
227        def server_changed(self,autocompleted_server_ip,server_instance_id,variable_name):
228
229                if server_instance_id==self.server_instance_id:
230
231                        if self.variables["REMOTE_VARIABLES_SERVER"] [u'value']!=autocompleted_server_ip:
232                                return False
233
234                        print "[VariablesManager] Server instance ID validated"
235
236                        t=threading.Thread(target=self.execute_trigger,args=(variable_name,))
237                        t.daemon=True
238                        t.start()
239                       
240                        return True
241                       
242                else:
243                       
244                        return False
245               
246        #def server_changed
247       
248       
249        def execute_trigger(self,variable_name):
250               
251                if variable_name in self.variables_triggers:
252                        for i in self.variables_triggers[variable_name]:
253                                class_name,function=i
254                                try:
255                                        print "[VariablesManager] Executing %s.%s trigger..."%(class_name,function.im_func.func_name)
256                                        function()
257                                except Exception as e:
258                                        print e
259                                        pass
260               
261        #def execute_trigger
262       
263       
264        def register_trigger(self,variable_name,class_name,function):
265               
266                if variable_name not in self.variables_triggers:
267                        self.variables_triggers[variable_name]=[]
268                       
269                self.variables_triggers[variable_name].append((class_name,function))
270               
271        #def register_trigger
272       
273       
274        def backup(self,dir="/backup"):
275               
276                try:
277               
278                        #file_path=dir+"/"+self.get_time()+"_VariablesManager.tar.gz"
279                        file_path=dir+"/"+get_backup_name("VariablesManager")
280                               
281                        tar=tarfile.open(file_path,"w:gz")
282                       
283                        tar.add(VariablesManager.VARIABLES_DIR)
284                       
285                        tar.close()
286                       
287                        return [True,file_path]
288                       
289                except Exception as e:
290                        return [False,str(e)]
291               
292        #def backup
293
294       
295        def restore(self,file_path=None):
296
297
298                if file_path==None:
299                        for f in sorted(os.listdir("/backup"),reverse=True):
300                                if "VariablesManager" in f:
301                                        file_path="/backup/"+f
302                                        break
303
304                try:
305
306                        if os.path.exists(file_path):
307                               
308                                tmp_dir=tempfile.mkdtemp()
309                                tar=tarfile.open(file_path)
310                                tar.extractall(tmp_dir)
311                                tar.close()
312                               
313                                if not os.path.exists(VariablesManager.VARIABLES_DIR):
314                                        os.mkdir(VariablesManager.VARIABLES_DIR)
315                               
316                                for f in os.listdir(tmp_dir+VariablesManager.VARIABLES_DIR):
317                                        tmp_path=tmp_dir+VariablesManager.VARIABLES_DIR+f
318                                        shutil.copy(tmp_path,VariablesManager.VARIABLES_DIR)
319                                       
320                                self.load_json(None)
321                                               
322                                return [True,""]
323                               
324                except Exception as e:
325                               
326                        return [False,str(e)]
327               
328        #def restore
329       
330        def log(self,txt):
331               
332                try:
333                        f=open(VariablesManager.LOG,"a")
334                        txt=str(txt)
335                        f.write(txt+"\n")
336                        f.close()
337                except Exception as e:
338                        pass
339               
340        #def log
341       
342        def listvars(self,extra_info=False,custom_dic=None):
343                ret=""
344               
345                try:
346               
347                        if custom_dic==None:
348                                custom_dic=self.variables
349                        for variable in custom_dic:
350                                if type(custom_dic[variable])==type({}) and "root_protected" in custom_dic[variable] and custom_dic[variable]["root_protected"]:
351                                        continue
352                                value=self.get_variable(variable)
353                                if value==None:
354                                        continue
355                                ret+=variable+ "='" + str(value).encode("utf-8") + "';\n"
356                                if extra_info:
357                                        ret+= "\tDescription: " + self.variables[variable][u"description"] + "\n"
358                                        ret+="\tUsed by:\n"
359                                        for depend in self.variables[variable][u"packages"]:
360                                                ret+= "\t\t" + depend.encode("utf-8") + "\n"
361                       
362                        return ret.strip("\n")
363                except Exception as e:
364                        return str(e)
365                                       
366        #def listvars
367       
368        def calculate_variable(self,value):
369                pattern="_@START@_.*?_@END@_"
370                variables=[]
371               
372                ret=re.findall(pattern,value)
373               
374                for item in ret:
375                        tmp=item.replace("_@START@_","")
376                        tmp=tmp.replace("_@END@_","")
377                        variables.append(tmp)
378               
379                for var in variables:
380                        value=value.replace("_@START@_"+var+"_@END@_",self.get_variable(var))
381                       
382                return value
383               
384        #def remove_calculated_chars
385       
386        def add_volatile_info(self):
387               
388                for item in self.variables:
389               
390                        if not self.variables[item].has_key("volatile"):
391                                self.variables[item]["volatile"]=False
392               
393        #def add_volatile_info
394
395       
396        def showvars(self,var_list,extra_info=False):
397               
398                ret=""
399               
400                for var in var_list:
401                        ret+=var+"='"
402                        if self.variables.has_key(var):
403                                try:
404                                        ret+=self.variables[var][u'value'].encode("utf-8")+"';\n"
405                                except Exception as e:
406                                        #it's probably something old showvars couldn't have stored anyway
407                                        ret+="';\n"
408                                if extra_info:
409                                        ret+= "\tDescription: " + self.variables[var][u"description"] + "\n"
410                                        ret+="\tUsed by:\n"
411                                        for depend in self.variables[var][u"packages"]:
412                                                ret+= "\t\t" + depend.encode("utf-8") + "\n"
413                        else:
414                                ret+="'\n"
415                                               
416                return ret.strip("\n")
417               
418        #def  showvars
419
420       
421        def get_variables(self):
422
423                return self.variables
424               
425        #def get_variables
426               
427       
428        def load_json(self, file=None):
429
430                self.variables={}
431               
432                if file!=None:
433               
434                        try:
435                               
436                                f=open(file,"r")
437                                data=json.load(f)
438                                f.close()
439                                self.variables=data
440                               
441                                #return [True,""]
442                               
443                        except Exception as e:
444                                print(str(e))
445                                #return [False,e.message]
446                               
447                for file in os.listdir(VariablesManager.VARIABLES_DIR):
448                        try:
449                                sys.stdout.write("\t[VariablesManager] Loading " + file + " ... ")
450                                f=open(VariablesManager.VARIABLES_DIR+file)     
451                                data=json.load(f)
452                                f.close()
453                                self.variables[file]=data[file]
454                                print("OK")
455                        except Exception as e:
456                                print("FAILED ["+str(e)+"]")
457                               
458                return [True,""]
459               
460        #def load_json
461       
462        def read_inbox(self, force_write=False):
463               
464                '''
465                        value
466                        function
467                        description
468                        packages
469                '''
470               
471                if self.variables_ok:
472               
473                        if os.path.exists(VariablesManager.INBOX):
474                               
475                                for file in os.listdir(VariablesManager.INBOX):
476                                        file_path=VariablesManager.INBOX+file
477                                        print "[VariablesManager] Adding " + file_path + " info..."
478                                        try:
479                                                f=open(file_path,"r")
480                                                data=json.load(f)
481                                                f.close()
482                                               
483                                                for item in data:
484                                                        if self.variables.has_key(item):
485                                                                for key in data[item].keys():
486                                                                        if not self.variables[item].has_key(unicode(key)):
487                                                                                self.variables[item][unicode(key)] = data[item][key]
488                                                                if data[item].has_key(unicode('function')):
489                                                                        self.variables[item][unicode('function')] = data[item][u'function']
490                                                                for depend in data[item][u'packages']:
491                                                                        if depend not in self.variables[item][u'packages']:
492                                                                                self.variables[item][u'packages'].append(depend)
493                                                               
494                                                                if "force_update" in data[item] and data[item]["force_update"]:
495                                                                        self.variables[item][u'value']=data[item][u'value']
496                                                        else:
497                                                                self.variables[item]=data[item]
498
499                                       
500                                        except Exception as e:
501                                                print e
502                                                #return [False,e.message]
503                                        os.remove(file_path)
504                               
505                                if force_write:
506                                        try:
507                                                self.add_volatile_info()
508                                                self.write_file()
509                                        except Exception as e:
510                                                print(e)
511                                               
512               
513                return [True,""]
514                               
515        #def read_inbox
516
517       
518        def empty_trash(self,force_write=False):
519               
520               
521                if self.variables_ok:
522               
523                        for file in os.listdir(VariablesManager.TRASH):
524                                file_path=VariablesManager.TRASH+file
525                                #print "[VariablesManager] Removing " + file_path + " info..."
526                                try:
527                                        f=open(file_path,"r")
528                                        data=json.load(f)
529                                        f.close()
530                                       
531                                        for item in data:
532                                                if self.variables.has_key(item):
533                                                        if data[item][u'packages'][0] in self.variables[item][u'packages']:
534                                                                count=0
535                                                                for depend in self.variables[item][u'packages']:
536                                                                        if depend==data[item][u'packages'][0]:
537                                                                                self.variables[item][u'packages'].pop(count)
538                                                                                if len(self.variables[item][u'packages'])==0:
539                                                                                        self.variables.pop(item)
540                                                                                break
541                                                                        else:
542                                                                                count+=1
543                                                       
544                                        #os.remove(file_path)
545                                       
546                                       
547                                except Exception as e:
548                                        print e
549                                        pass
550                                        #return [False,e.message]
551                                os.remove(file_path)
552                       
553                        if force_write:
554                                try:   
555                                        self.write_file()
556                                except Exception as e:
557                                        print(e)
558                               
559                return [True,'']
560                       
561               
562        #def empty_trash
563       
564
565        def get_variable_list(self,variable_list,store=False,full_info=False):
566               
567                ret={}
568                if variable_list!=None:
569                        for item in variable_list:
570                                try:
571                                        ret[item]=self.get_variable(item,store,full_info)
572                                        #if ret[item]==None:
573                                        #       ret[item]=""
574                                except Exception as e:
575                                        print e
576
577                return ret
578               
579        #def get_variable_list
580       
581
582        def get_variable(self,name,store=False,full_info=False,key=None):
583       
584                global master_password
585               
586                if name in self.variables and self.variables[name].has_key("root_protected") and self.variables[name]["root_protected"] and key!=master_password:
587                        return None
588                       
589                if name in self.variables and self.variables[name].has_key("function"):
590                        try:
591                                if not full_info:
592                                        if (type(self.variables[name][u"value"])==type("") or  type(self.variables[name][u"value"])==type(u"")) and self.variables[name][u"value"].find("_@START@_")!=-1:
593                                                #print "I have to ask for " + name + " which has value: " + self.variables[name][u'value']
594                                                value=self.calculate_variable(self.variables[name][u"value"])
595                                        else:
596                                                value=self.variables[name][u"value"]
597                                        #return str(value.encode("utf-8")
598                                        if type(value)==type(u""):
599                                                try:
600                                                        ret=value.encode("utf-8")
601                                                        return ret
602                                                except:
603                                                        return value
604                                        else:
605                                                return value
606                                else:
607                                        variable=self.variables[name].copy()
608                                        variable["remote"]=False
609                                        if type(variable[u"value"])==type(""):
610                                                if variable[u"value"].find("_@START@_")!=-1:
611                                                        variable["original_value"]=variable[u"value"]
612                                                        variable[u"value"]=self.calculate_variable(self.variables[name][u"value"])
613                                                        variable["calculated"]=True
614                                        return variable
615                        except:
616                                return None
617                else:
618                        if self.variables.has_key("REMOTE_VARIABLES_SERVER") and self.variables["REMOTE_VARIABLES_SERVER"][u"value"]!="" and self.variables["REMOTE_VARIABLES_SERVER"][u"value"]!=None:
619                                try:
620                                        server_ip=socket.gethostbyname(self.variables["REMOTE_VARIABLES_SERVER"][u"value"])
621                                except:
622                                        return None
623                                if self.get_ip()!=server_ip:
624                                        for count in range(0,3):
625                                                try:
626
627                                                        server=xmlrpclib.ServerProxy("https://"+server_ip+":9779",allow_none=True)
628                                                        var=server.get_variable("","VariablesManager",name,True,True)
629                                                       
630                                                        if var==None:
631                                                                return None
632                                                        if (var!=""  or type(var)!=type("")) and store:
633
634                                                                self.add_variable(name,var[u"value"],var[u"function"],var[u"description"],var[u"packages"],False)
635                                                                return self.get_variable(name,store,full_info)
636                                                        else:
637                                                                if full_info:
638                                                                        var["remote"]=True
639                                                                        return var
640                                                                else:
641                                                                        return var["value"]
642                                                               
643                                                except Exception as e:
644                                                        time.sleep(1)
645                                       
646                                        return None
647                                else:
648                                        return None
649                        else:
650                               
651                                return None
652                       
653        #def get_variable
654
655       
656        def set_variable(self,name,value,depends=[],force_volatile_flag=False):
657
658                if name in self.variables:
659                       
660                        if value == self.variables[name][u"value"]:
661                                return [True,"Variable already contained that value"]
662                       
663                        if type(value)==type(""):
664                                self.variables[name][u"value"]=unicode(value).encode("utf-8")
665                        else:
666                                self.variables[name][u"value"]=value
667
668                        if len(depends)>0:
669                                for depend in depends:
670                                        self.variables[unicode(name).encode("utf-8")][u"packages"].append(depend)
671                       
672                        if not force_volatile_flag:
673                                self.write_file()
674                        else:
675                               
676                                self.variables[name]["volatile"]=True
677                                if "function" not in self.variables["name"]:
678                                        self.variables[name]["function"]=""
679                                if "description" not in self.variables["name"]:
680                                        self.variables[name]["description"]=""
681                       
682                        t=threading.Thread(target=self.notify_changes,args=(name,))
683                        t.daemon=True
684                        t.start()
685                       
686                        return [True,""]
687                else:
688                        return [False,"Variable not found. Use add_variable"]
689               
690               
691        #def set_variable
692
693       
694        def add_variable(self,name,value,function,description,depends,volatile=False,root_protected=False):
695
696                if name not in self.variables:
697                        dic={}
698                        if type(value)==type(""):
699                                dic[u"value"]=unicode(value).encode("utf-8")
700                        else:
701                                dic[u"value"]=value
702                        dic[u"function"]=function
703                        dic[u"description"]=unicode(description).encode("utf-8")
704                        if type(depends)==type(""):
705                                dic[u"packages"]=[unicode(depends).encode("utf-8")]
706                        elif type(depends)==type([]):
707                                dic[u"packages"]=depends
708                        else:
709                                dic[u"packages"]=[]
710                        dic["volatile"]=volatile
711                        dic["root_protected"]=root_protected
712                        self.variables[unicode(name)]=dic
713                        if not volatile:
714                                self.write_file()
715                        return [True,""]
716                else:
717                        return [False,"Variable already exists. Use set_variable"]
718                       
719        #def add_variable
720
721
722        def write_file(self,fname=None):
723               
724                try:
725                        while os.path.exists(VariablesManager.LOCK_FILE):
726                                time.sleep(2)
727                               
728                        f=open(VariablesManager.LOCK_FILE,"w")
729                        f.close()
730                        tmp_vars={}
731                        for item in self.variables:
732                                if self.variables[item].has_key("volatile") and self.variables[item]["volatile"]==False:
733                                        tmp_vars[item]=self.variables[item]
734                                       
735                        for item in tmp_vars:
736                               
737                                tmp={}
738                                tmp[item]=tmp_vars[item]
739                                f=open(VariablesManager.VARIABLES_DIR+item,"w")
740                                data=unicode(json.dumps(tmp,indent=4,encoding="utf-8",ensure_ascii=False)).encode("utf-8")
741                                f.write(data)
742                                f.close()
743                               
744                                if "root_protected" in tmp_vars[item]:
745                                        if tmp_vars[item]["root_protected"]:
746                                                self.chmod(VariablesManager.VARIABLES_DIR+item,0600)
747                                               
748                                               
749                        os.remove(VariablesManager.LOCK_FILE)
750                        return True
751                               
752                       
753                except Exception as e:
754                        os.remove(VariablesManager.LOCK_FILE)
755                        print (e)
756                        return False
757               
758        #def write_file
759
760
761        def chmod(self,file,mode):
762                prevmask = os.umask(0)
763                try:
764                        os.chmod(file,mode)
765                        os.umask(prevmask)
766                        return True
767                except Exception as e:
768                        print e
769                        os.umask(prevmask)
770                        return False
771                       
772        #def chmod
773       
774       
775        def init_variable(self,variable,args={},force=False,full_info=False):
776
777                try:
778                        funct=self.variables[variable]["function"]
779                        mod_name=funct[:funct.rfind(".")]
780                        funct_name=funct[funct.rfind(".")+1:]
781                        funct_name=funct_name.replace("(","")
782                        funct_name=funct_name.replace(")","")
783                        mod=importlib.import_module(mod_name)
784                        ret=getattr(mod,funct_name)(args)
785                        ok,exc=self.set_variable(variable,ret)
786                        if ok:
787                                return (True,ret)
788                        else:
789                                return (False,ret)
790                except Exception as e:
791                        return (False,e)
792               
793        #def init_variable
794       
795       
796#class VariablesManager
797
798
799if __name__=="__main__":
800       
801        vm=VariablesManager()
802       
803        print vm.listvars()
804        print vm.init_variable("name_center")
805        args={}
806        args["iface"]="eth0"
807        print vm.init_variable("SERVER_IP",args)
808        print vm.write_file()
809        #print vm.get_variable("VARIABLE2",full_info=True)
810        #print vm.get_variable("VARIABLE3",full_info=True)
811        #print vm.showvars(var_list)
812       
813       
814               
815               
816               
817       
818       
819       
Note: See TracBrowser for help on using the repository browser.