import socket import subprocess import sys import threading import time import datetime import json import os import signal import shutil import tempfile import tarfile import apt_pkg class LmdServer: def __init__(self): self.last_job_id=0; self.joblist=[] self.global_listeners = {} self.thread_jobs = {} # Threading Multicast process to all listeners self.multicast_thread = threading.Thread() self.logfolder = '/run/lmdserver' self.ltsp_path = '/opt/ltsp' self.locks = {} # Clean log folder and create if (os.path.exists(self.logfolder)): shutil.rmtree(self.logfolder) os.mkdir(self.logfolder) pass #def __init__ def set_default_boot(self,imgid): dbm = objects['LmdBootManager'].getDefaultBootImage() if dbm['default_boot'] == "": objects['LmdBootManager'].setDefaultBootImage(imgid) def check_chroot_exists(self, name): ''' Check if exists chroot /opt/ltsp/name and /opt/ltsp/images/name.img DATE: 03/04/2014 ''' path=os.path.join("/opt/ltsp",name) imagepath=os.path.join("/opt/ltsp/images",name+".img") #print "cheking "+name if(os.path.isdir(path)): #print "111" return {"status": True, "error": 'chroot_exists'} if (os.path.isfile(imagepath)): #print "22222" return {"status": True, "error": 'image_exists'} return {"status": False} def create_image(self, ip, port, imgid, name, template, description='', bgimg='', srv_ip='127.0.0.1'): try: # creates an image from an specified template # Check if template exists path="/etc/ltsp/templates/"+template; if(os.path.isfile(path)): extraLliurexOpts="--accept-unsigned-packages --mirroronnet --purge-chroot" # if template exists, create an image print "[LmdServer] Create_image from "+path command="ltsp-build-client --config "+path+" "+extraLliurexOpts+" --chroot "+imgid+"&& service nbd-server restart"; metadata = {'id':imgid, 'name' : name, 'desc' : description , "template" : template, 'img': bgimg, 'ltsp_fatclient': 'undefined', 'ldm_session': 'default', 'fat_ram_threshold': 'default', 'lmd_extra_params':'' } metadata_string = unicode(json.dumps(metadata,indent=4,encoding="utf-8",ensure_ascii=False)).encode("utf-8") objects['LmdImageManager'].setImage(imgid,metadata_string) self.set_default_boot(imgid) #command="cat "+path+"; sleep 15; cat "+path+"; echo 'command is: ltsp-build-client --config "+path+" "+extraLliurexOpts+" --chroot "+imgid+"'"; #command="/tmp/miscript" #command="tree /" result=self.do_command(ip, port, command, srv_ip,target=imgid) return result else: return "{'status':'false', 'error':'template does not exists'}" pass except Exception as e: print "Except: "+str(e) return e def refresh_image(self, ip, username, password, port, imgid, srv_ip='127.0.0.1'): # Rebuild img file for imgid import xmlrpclib try: # umount anything path="/opt/ltsp/"+imgid server = xmlrpclib.ServerProxy("https://localhost:9779") server.umount_chroot((username,password),'LmdImageManager',path); # Let's rebuild image print "[LmdServer] Refreshing image for "+str(imgid) command="ltsp-update-kernels "+imgid+" && ltsp-update-image "+imgid+"&& service nbd-server restart"; result=self.do_command(ip, port, command, srv_ip,target=imgid) return {"status": True, "msg": 'Image Updated'} pass except Exception as e: print "Except: "+str(e) return {"False": True, "msg": str(e)} def export_image(self, ip, port, imgid, srv_ip='127.0.0.1'): try: # creates an image from an specified template # Check if template exists print "[LmdServer] Export_image from "+imgid name_file = str(imgid) + "_" + time.strftime("%Hh%Mm%Ss_%d%m%Y") + '.tgz' command="ltsp-import-export export /tmp/"+ name_file +" "+str(imgid); result=self.do_command(ip, port, command, srv_ip,target=imgid) return {"status": True, "msg": str(result)} pass except Exception as e: print "Except: "+str(e) return {"status": False, "msg": str(e)} def import_image(self, ip, port, imgid, path, srv_ip='127.0.0.1'): try: if(ip==srv_ip): print "[LmdServer] Import_image from ",path, " to ", imgid tar = tarfile.open(path) l = tar.getnames() folder = l[0] imgid = folder f = tar.extractfile(folder + '/' + folder + '.json') exec("json_content="+"".join(f.readlines())) try: new_name = folder count = 1 while os.path.exists(os.path.join(self.ltsp_path,new_name)): new_name = folder + "_" + str(count) count += 1 extra_opts = "" if folder != new_name: extra_opts = new_name except Exception as e: extra_opts = "" command="ltsp-import-export import "+str(path)+" "+folder+" "+extra_opts+" && service nbd-server restart"; if extra_opts != "": imgid = extra_opts json_content['id'] = imgid json_content['name'] = imgid json_file = open('/etc/ltsp/images/' + imgid + '.json','w') data = unicode(json.dumps(json_content,indent=4,encoding='utf-8',ensure_ascii=False)).encode('utf-8') json_file.write(data) json_file.close() self.set_default_boot(imgid) result=self.do_command(ip, port, command, srv_ip,target=imgid) return {"status": True, "msg": str(result)} pass else: return {"status": False, "msg": "Not in server"} except Exception as e: print "Except: "+str(e) return {"status": False, "msg": str(e)} def deploy_minimal_client(self, ip, port, srv_ip='127.0.0.1'): try: command="/usr/sbin/mini-light-client.py"; imgid="mini-light-client" lng="es_ES.UTF-8" language="es_ES" # After that, set image as available metadata = {'status':'enabled-non-editable', 'id':'mini-light-client', 'name':'Client Lleuger Minim', 'template':'Default by LMD', 'desc':'Minimal thin client -not fat- for Lliurex LTSP.', 'img':'llx-client13.png', 'ltsp_fatclient': 'false', 'ldm_session': 'gnome-session-fallback', 'fat_ram_threshold': 'default', 'lmd_extra_params':'XKBLAYOUT=es LDM_LANGUAGE="%s" LOCALE_DIR=%s'%(lng,language)} metadata_string = unicode(json.dumps(metadata,indent=4,encoding="utf-8",ensure_ascii=False)).encode("utf-8") objects['LmdImageManager'].setImage('mini-light-client',metadata_string) self.set_default_boot(imgid) result=self.do_command(ip, port, command, srv_ip,target=imgid) print "[LMDServer] Writing lts.conf for mini-light-client" f=open('/var/lib/tftpboot/ltsp/mini-light-client/lts.conf', 'w') f.write('[Default]\n') f.write('LDM_LANGUAGE=es_ES.UTF-8\n') f.write('LTSP_FATCLIENT=false\n') f.close() try: f=open("/etc/default/locale") lines=f.readlines() f.close() for line in lines: if "LANG=" in line: lng=line.strip().split("=")[1].strip("'").strip('"') if "es_ES" in lng: language="es" if "ca_ES" in lng: language="ca_ES@valencia" except: pass return {"status": True, "msg": str(result)} pass except Exception as e: print "Except: "+str(e) return {"status": False, "msg": str(e)} def do_command(self, ip, port, command, srv_ip='127.0.0.1',target=None): try: # Add Job '''job={ 'job_id':str(self.last_job_id), 'srv_ip': None, # Server (me) IP addr 'process': None, 'status':'started', 'msg':'', # Error code 'target':'None', 'command':command, 'started_by':None, 'listeners': [{'ip':ip, 'port':port, 'socket':None}], 'filepipe': '' }''' call_info = _n4d_get_user() job={ 'job_id':str(self.last_job_id), 'srv_ip': srv_ip, 'process': None, 'status':'started', 'msg':None, 'target': target, 'command':command, 'started_by':str(ip), 'listeners': [], 'filepipe': '', 'seek' : 0, 'method':call_info['method'], 'class': call_info['class'] } lock = threading.Lock() self.locks[job['job_id']]={} self.locks[job['job_id']]['lock'] = lock self.locks[job['job_id']]['lock'].acquire() # Exec command # proc = subprocess.Popen([command], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, preexec_fn=os.setsid) # Prepare file pipe temp = tempfile.NamedTemporaryFile(prefix='pipe_', dir=self.logfolder, delete=False) # New exec command, ignoring stderr stdin for now proc = subprocess.Popen([command], shell=True, stdout=temp, preexec_fn=os.setsid) # Add job to tasklist job['process']=proc # Assotiate process to job job['status']="running" job['filepipe'] = temp.name self.joblist.append(job) #Adding job self.add_listener_to_job(ip, port, job) # Adding the client ip:port to listeners list ## PAUSA PARA PROVOCAR UNA CONDICION DE CARRERA ## ## time.sleep(2) ## FIN DE LA PAUSA ## #self.joblist.append(job) #Adding job # Moved before add_listener_to_job # Increase last_job_id self.last_job_id=self.last_job_id+1 # Multicast process to all listeners print "[LmdServer] WAITING ...:"+str(datetime.datetime.now()) ret=job['process'].poll() while ret is None: time.sleep(1) ret=job['process'].poll() #temp.close() job['status']="finished" job['msg']=str(ret) print "[LmdServer] END WAIT AT"+str(datetime.datetime.now()) print "[LmdServer] FINISHING!!!, return code: "+str(ret) print "2" # Force umount (to avoid morrir destruction in mirrononnet) proc=subprocess.call(["/usr/share/lmd-scripts/umount-chroot.sh"]) # Sending last line to log for all listeners line="Finished with status:"+job['status']+" and Response: "+job['msg']+" \n" aux = open(job['filepipe'],'a') aux.writelines(line) aux.close() # Append result of job and release mutex. Now all inform_me return finish self.locks[job['job_id']]['result'] = str(ret) self.locks[job['job_id']]['lock'].release() return str(ret) except Exception as e: job['status']="Error" print '[LmdServer]',e if (ret is None): job['msg']="Err code None (running)" elif (ret<0): job['msg']="Interrupted by signal: "+str(ret) else: job['msg']="Aparently all went well: "+str(ret) # Append result of job and release mutex. Now all inform_me return finish self.locks[job['jobid']]['result'] = str(ret) self.locks[job['jobid']]['lock'].release() return str(e) def inform_me(self,job_id): ''' Return result of job when finish ''' self.locks[job_id]['lock'].acquire() self.locks[job_id]['lock'].release() return {'status':True,'msg':self.locks[job_id]['result']} def add_listener_to_job(self, ip, port, job): ''' Description: * Internal Function * Adds a listener identified by an ip and a port to a job object How it Works: * Creates a socket to send information to a listening socket in any client Last update: * 5/02/2014: Added Functionality * 2/04/2014: Add reference with job_id ''' try: # Create a socket for new listener srv=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # connection to socket to send log srv.connect((ip, int(port))) # Defines a new listener listener={'ip':ip, 'port':port, 'socket':srv} # Add listener to job job['listeners'].append(listener) print "[LmdServer] add_listener_to_job: adding "+str(listener)+"to listeners for job "+str(job['job_id']) # Sending status of job listener['socket'].send("Start listening on "+str(listener['port'])+"") listener['socket'].send(" Task:"+str(job['command'])+"") listener['socket'].send(" Started by: "+str(job['started_by'])+"") # Add reference with job_id tuple_ip_port = (str(ip),str(port)) self.global_listeners[tuple_ip_port] = job['job_id'] #print "Len",len(self.global_listeners) #print "isAlive",self.multicast_thread.isAlive() if not self.multicast_thread.isAlive(): #print "lanzando el thread" self.multicast_thread = threading.Thread(target=self.Multicast, args=()) self.multicast_thread.daemon = True self.multicast_thread.start() pass return port except Exception as e: print("[LmdServer] add_listener_to_job Exception: "+str(e)) return -1 pass def add_listener(self, ip, port, job_id): ''' Description: * n4d method * Adds a listener identified by an ip and a port to a job object identified by its id How it Works: * Search whic job has certain job_id and adds a listener to it Last update: * 5/02/2014: Added Functionality ''' from operator import itemgetter #print ("[add_listener DEBUG] address: "+str(ip)+":"+str(port)+" job id: "+str(job_id)) try: # Get job identified by job_id current_job_index=map(itemgetter('job_id'), self.joblist).index(str(job_id)) current_job=self.joblist[current_job_index] # Add listener to current_job self.add_listener_to_job(ip, port, current_job) except Exception as e: print ("[LmdServer] add_listener Exception "+str(e)) pass def send(self, ip, job_id, info): ''' receives info to send to stdin ''' from operator import itemgetter try: current_job_index=map(itemgetter('job_id'), self.joblist).index(str(job_id)) current_job=self.joblist[current_job_index] # Get listener identified by (ip:port) #current_job['process'].stdin.writelines(str(info)) if(current_job['status']=='running'): current_job['process'].stdin.write(str(info)+"\n") #current_job['process'].write(info); except Exception as e: print ("ERROR writing to process: "+str(e)); def remove_listener(self, ip, port, job_id): ''' Description: * n4d method * Removes listener identified by an ip and a port to a job object identified by its id How it Works: * Search whic job has certain job_id (if specified) and remove its * If job_id is not specified, get it from ip:port Last update: * 6/02/2014: Added Functionality * 2/04/2014: Added checking for job_id and close socket ''' from operator import itemgetter try: # If job_id is None, search job_ib by ip and port tuple_ip_port = (str(ip),str(port)) if job_id == None: job_id = self.global_listeners[tuple_ip_port] # Get job identified by job_id current_job_index=map(itemgetter('job_id'), self.joblist).index(job_id) current_job=self.joblist[current_job_index] # Get listener identified by (ip:port) current_listener_index=map(itemgetter('ip','port'), current_job['listeners']).index((ip,int (port, base=10))) # Close port and remove listener to current_job current_job['listeners'][current_listener_index]['socket'].close() current_job['listeners'].remove(current_job['listeners'][current_listener_index]) #remove listener reference self.global_listeners.pop(tuple_ip_port) return True except Exception as e: print ("[LmdServer] remove_listener Exception "+str(e)) return False pass def cancel_job(self, ip, port, job_id): ''' Description: * n4d method * TO DO ... Same functionality that remove_listener and in addition, kills process identified by job ''' # Remove listener from operator import itemgetter try: # If job_id is None, search job_ib by ip and port tuple_ip_port = (str(ip),str(port)) if job_id == None: job_id = self.global_listeners[tuple_ip_port] # Get job identified by job_id current_job_index=map(itemgetter('job_id'), self.joblist).index(job_id) current_job=self.joblist[current_job_index] # Get listener identified by (ip:port) current_listener_index=map(itemgetter('ip','port'), current_job['listeners']).index((ip,int (port, base=10))) # Kill process (only addition to remove_listener) os.killpg(current_job['process'].pid,signal.SIGKILL) current_job['status']='finished' # Close port and remove listener to current_job current_job['listeners'][current_listener_index]['socket'].close() current_job['listeners'].remove(current_job['listeners'][current_listener_index]) #remove listener reference self.global_listeners.pop(tuple_ip_port) return True except Exception as e: print ("[LmdServer] cancel_job Exception "+str(e)) return False pass def getJobList(self): ''' Description: * N4D Method * Return JSON with the job list ''' #import urllib #ret= urllib.quote(str(self.joblist)[1:-1]) #ret= (str(self.joblist)[1:-1]).replace("'", "\""); #ret= (str(self.joblist)).replace("'", "\""); '''ret=ret.replace("<", "\""); ret=ret.replace(">", "\"");''' ret='['; count=0; for job in self.joblist: if (count>0): ret=ret+',' ret=ret+'{"job_id":"'+str(job['job_id'])+'"' ret=ret+', "srv_ip":"'+str(job['srv_ip'])+'"' ret=ret+', "status":"'+str(job['status'])+'"' ret=ret+', "msg":"'+str(job['msg'])+'"' ret=ret+', "target":"'+str(job['target'])+'"' ret=ret+', "command":"'+str(job['command'])+'"' ret=ret+', "started_by":"'+str(job['started_by'])+'"}' count=count+1 #print "*********************" #print "Local listeners" #print job['listeners'] #print "*********************" ret=ret+']' #print (ret) #print "*********************" #print "globals listeners" #print self.global_listeners #print "*********************" return str(ret) def Multicast(self): ''' Description: * Internam method * Multicast the output of all processes to its listeners How it works: * Traverses the list of jobs and write its output to all its listeners * Last Update: 13/02/2014 ''' try: while len(self.global_listeners) > 0 : counter = 0 #print "joblist",self.joblist #print "global_listeners",self.global_listeners for job in self.joblist: if True : #job['status'] != "finished": counter += 1 try: if not self.thread_jobs.has_key(job['job_id']): self.thread_jobs[job['job_id']] = threading.Thread(target=self.send_info_by_socket, args=(job,)) self.thread_jobs[job['job_id']].daemon = True self.thread_jobs[job['job_id']].start() except Exception as e: print e if counter == 0: break time.sleep(1) except Exception as e: print "[LmdServer] EXCEPTION in Multicast: "+str(e) def send_info_by_socket(self,job): if not os.path.exists(job['filepipe']): return False pipe = open(job['filepipe'],'r') pipe.seek(job['seek'],0) try: line = pipe.readline() while (line and len(self.global_listeners)>0): for listener in job['listeners']: if(listener['socket']!=None): try: listener['socket'].send(line) except Exception as e: print "[LmdServer] EXCEPTION in Multicast internal loop: "+str(e) continue line=pipe.readline() job['seek'] = pipe.tell() except: pass if self.thread_jobs.has_key(job['job_id']): self.thread_jobs.pop(job['job_id']) def getLastJobId(self): return self.last_job_id def getJobStatus(self, jobid): for i in self.joblist: if i['job_id']==jobid: return {"status": True, "msg": str(i['status'])} return {"status": False, "msg": 'bad luck, guy'} def check_lmd_status(self): ''' Description: * N4D Method * check status of lmd ''' import os.path if (os.path.isfile("/tmp/.lmd-editing-chroot")): return {"status": True, "lmd_status": 'lmd-editing-chroot'} else: return {"status": True, "lmd_status": 'lmd-chroot-available'} def chek_minimal_client(self): if (os.path.isfile("/etc/ltsp/images/mini-light-client.json") and os.path.isfile("/opt/ltsp/images/mini-light-client.img") ): return {"status": True} else: return {"status": False} def get_versions_13(self): ''' Checks if there is any old image un system (13.06) ''' ltsp_dir="/opt/ltsp/" nbd_dir="/etc/nbd-server/conf.d/" ret_list=[]; for name in os.listdir(ltsp_dir): dir=ltsp_dir+name needs_update=False if (name!='images' and os.path.isdir(dir)): proc = subprocess.Popen(["chroot "+dir +" lliurex-version"], stdout=subprocess.PIPE, shell=True) (out, err) = proc.communicate() if ((not "14.06" in out) and out!="" ): # check nbd... for nbd_descriptor in os.listdir(nbd_dir): if (self.line_in_file("["+ltsp_dir+name+"]", nbd_dir+nbd_descriptor)): needs_update=True if (needs_update==True): ret_list.append(name) #ret_list.append("pajarito"); # TO DELETE #ret_list.append("perro"); # TO DELETE return ret_list def line_in_file(self, line_to_find, filename): ''' Aux function: ask for if certain line is contained in a file ''' fd = open(filename, 'r') lines = fd.readlines() fd.close() for line in lines: if str(line_to_find) in line: return True return False def update_images(self, ip, port, imagelist, srv_ip): try: imagelist_string = " ".join(imagelist) ##print imagelist," is ", type(imagelist) ### n4d-client -c LmdServer -m update_images -u joamuran -p lliurex -a 2 3 "['pajarito', 'perro']" # Prepare and launch command command="/usr/share/lmd-scripts/lmd-upgrade-images.sh "+imagelist_string; result=self.do_command(ip, port, command, srv_ip, None) # Add image description #imagelist=imagelist_string.replace("[","").replace("]","").replace(" ","").replace("'", "").replace(","," "); #print imagelist_string; print imagelist; #print imagelist.split(" "); for i in imagelist: metadata = {'id':i, 'name' : i, 'desc' : 'Upgraded from Lliurex 13.06', 'template' : 'none', 'ltsp_fatclient': 'undefined', 'ldm_session': 'default', 'fat_ram_threshold':'default', 'lmd_extra_params':''} metadata_string = unicode(json.dumps(metadata,indent=4,encoding="utf-8",ensure_ascii=False)).encode("utf-8") objects['LmdImageManager'].setImage(i,metadata_string) self.set_default_boot(imgid) return {"status": True} except Exception as e: print "Exception", e return {"status": False, 'msg':str(e)} def delete_images(self,imagelist): try: for img in imagelist: print img path_chroot=os.path.join("/opt/ltsp/", img) path_tftpboot=os.path.join("/var/lib/tftpboot/ltsp/", img) path_img=os.path.join("/opt/ltsp/images/", img+".img") path_nbd=os.path.join("/etc/nbd-server/conf.d/", "ltsp_"+img+".conf") if (os.path.exists(path_chroot)): shutil.rmtree(path_chroot) #print "deleting: ",path_chroot if (os.path.exists(path_tftpboot)): shutil.rmtree(path_tftpboot) if (os.path.exists(path_img)): #print "deleting: ",path_img shutil.rmtree(path_img); if (os.path.exists(path_nbd)): #print "deleting: ",path_nbd shutil.rmtree(path_nbd); return {"status": True, 'msg':'Finished'} except Exception as e: print "Exception", e return {"status": False, 'msg':str(e)} def LmdServerVersion(self): info=subprocess.check_output(["apt-cache","policy","lmd-server"]) lines=str(info).split('\n') version=lines[1][13:] return (version) def check_update_images(self): list_dirs = [ os.path.join(self.ltsp_path,x) for x in os.listdir(self.ltsp_path) if os.path.isdir(os.path.join(self.ltsp_path,x)) ] list_need_update = [] for chroot in list_dirs: available_file = os.path.join(chroot,'var','lib','dpkg','available') if os.path.exists(available_file): available_fd = open(available_file,'r') available_content = available_fd.readlines() try: pos = available_content.index('Package: lmd-client\n') version = None for i in range(pos + 1 ,len(available_content)): if available_content[i] == '\n': break if available_content[i].startswith('Version'): version = available_content[i][8:].strip() if version != None : apt_pkg.init() if apt_pkg.version_compare(version,'0.15') < 0 : list_need_update.append(os.path.basename(chroot)) except Exception as e: pass if len(list_need_update) > 0: return [True,list_need_update] else: return [False,[]]