Python wrapper for running instances of trisurf-ng
Samo Penic
2017-09-18 a8e8b1dbfb5873af8ccfc720a1d39c2b098fbcef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import argparse
import paramiko
from . import Remote
from . import trisurf
import socket
import os,sys
import tabulate
import subprocess,re
import psutil
#import http.server
#import socketserver
if sys.version_info>=(3,0):
    from urllib.parse import urlparse
    from . import WebTrisurf
else:
    from urlparse import urlparse
    from vtk import *
    
#import io
 
from IPython import embed
 
import __main__ as main
 
#Color definitions for terminal
class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'
 
#parses Command Line Arguments and returns the list of parsed values
def ParseCLIArguments(arguments):
    parser = argparse.ArgumentParser(description='Manages (start, stop, status) multiple simulation processes of trisurf according to the configuration file.')
    parser.add_argument('proc_no', metavar='PROC_NO', nargs='*',
                help='process number at host. If hostname is not specified, localhost is assumed. If no processes are specified all processes on all hosts are assumed.')
    action_group=parser.add_mutually_exclusive_group(required=True)
    action_group.add_argument('-c','--comment',nargs=1, help='append comment to current comment')
    action_group.add_argument('--analysis', nargs='+', help='runs analysis function defined in configuration file')
    action_group.add_argument('--delete-comment', help='delete comment',action='store_true')
    action_group.add_argument('-k','--kill','--stop','--suspend', help='stop/kill the process', action='store_true')
    action_group.add_argument('-r','--run','--start','--continue', help='start/continue process', action='store_true')
    action_group.add_argument('-s','--status',help='print status of the processes',action='store_true')
    action_group.add_argument('-v','--version', help='print version information and exit', action='store_true')
    action_group.add_argument('--web-server', type=int,metavar="PORT", nargs=1, help='EXPERIMENTAL: starts web server and never exist.')
    action_group.add_argument('--jump-to-ipython', help='loads the variables and jumps to IPython shell', action="store_true")
    action_group.add_argument('-p','--preview',help='preview last VTU shape',action='store_true')
    parser.add_argument('--force', help='if dangerous operation (killing all the processes) is requested, this flag is required to execute the operation. Otherwise, the request will be ignored.', action="store_true")
    parser.add_argument('-H', '--host', nargs='+', help='specifies which host is itended for the operation. Defauts to localhost for all operations except --status and --version, where all configured hosts are assumed.')
    parser.add_argument('--html', help='Generate HTML output', action="store_true")
    parser.add_argument('-n', nargs='+', metavar='PROC_NO', type=int, help='OBSOLETE. Specifies process numbers.')
    parser.add_argument('-R','--raw',help='print status and the rest of the information in raw format', action="store_true")
    parser.add_argument('-x','--local-only',help='do not attempt to contact remote hosts. Run all operations only on local machine',action='store_true')
    parser.add_argument('--originating-host',nargs=1,help='specify which host started the remote connections. Useful mainly fo internal functionaly of tsmgr and analyses.')
    parser.add_argument('--analysis-options', nargs='+', help='options passed down to analyses')
    args = parser.parse_args(arguments)
    return args
 
 
#gets version of trisurf currently running
def getTrisurfVersion():
    p = subprocess.Popen('trisurf --version', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    lines=p.stdout.readlines()
    version=re.findall(r'[0-9a-f]{7}(?:-dirty)?', lines[0].decode('ascii'))
    p.wait()
    if(len(version)):
        return version[0]
    else:
        return "unknown version"
 
 
 
def copyConfigAndConnect(hosts):
    print("Connecting to remote hosts and copying config files, tapes and snapshots")
    #create a list of files to be copied across all the remote hosts
    file_list=[]
    for h in hosts:
        for r in h['runs']:
            if(r.isFromSnapshot):
                file_list.append(r.snapshotFile)
            else:
                file_list.append(r.tapeFilename)
    file_list.append(main.__file__)
    for host in hosts:
        if(host['name'] !=socket.gethostname()): #if I am not the computer named in host name
            try:
                username=host['username']
            except:
                username=os.getusername() #default username is current user user's name
            try:
                port=host['port']
            except:
                port=22 #default ssh port
            rm=Remote.Connection(hostname=host['address'],username=username, port=port)
            rm.connect()
#            print ("Sendind file:"+main.__file__)
            if('remotebasepath' in host):
                remote_dir=host['remotebasepath']
            else:
                remote_dir='trisurf_simulations'
            rm.send_multiple_files_in_directory(file_list,remote_dir)
#            rm.send_file(main.__file__,'remote_control.py')
#            for run in host['runs']:
#                try:
#                    rm.send_file(run.tapeFile,run.tapeFile)
#                except:
#                    pass
#                try:
#                    rm.send_file(run.snapshotFile,run.snapshotFile)
#                except:
#                    pass
            host['_conn']= rm
    # we are connected to all hosts...
    return hosts
 
 
 
def getTargetRunIdxList(args):
    target_runs=list(map(int,args['proc_no']))
    if len(target_runs)==0:
        #check if obsolete -n flags have numbers
        target_runs=args['n']
        if target_runs==None:
            return None
    target_runs=list(set(target_runs))
    return target_runs
 
 
 
def status_processes(args,host):
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        target_runs=list(range(1,len(host['runs'])+1))
    report=[]
#    print("was here")
    for i in target_runs:
        line=host['runs'][i-1].getStatistics()
        line.insert(0,i)
        report.append(line)
    if(args['raw']):
        print(report)
    else:
        if(args['html']):
            tablefmt='html'
        else:
            tablefmt='fancy_grid'
        print(tabulate.tabulate(report,headers=["Run no.", "Run start time", "ETA", "Status", "PID", "Path", "Comment"], tablefmt=tablefmt))
    return
 
def run_processes(args,host):
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        target_runs=list(range(1,len(host['runs'])+1))
    for i in target_runs:
        host['runs'][i-1].start()
    return
 
def kill_processes(args,host):
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        if args['force']==True:
            target_runs=list(range(1,len(host['runs'])+1))
        else:
            print("Not stopping all processes on the host. Run with --force flag if you are really sure to stop all simulations")
            return
    for i in target_runs:
        host['runs'][i-1].stop()
    return
 
def comment_processes(args,host):
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        target_runs=list(range(1,len(host['runs'])+1))
    for i in target_runs:
        host['runs'][i-1].writeComment(args['comment'][0],'a')
    print("Comment added")
    return
 
def delete_comments(args,host):
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        if args['force']==True:
            target_runs=list(range(1,len(host['runs'])+1))
        else:
            print("Not deleting comments on all posts on the host. Run with --force flag if you are really sure to delete all comments")
            return
    for i in target_runs:
        host['runs'][i-1].writeComment("")
    print("Comment deleted")
    return
 
 
def start_web_server(args,host):
    print('Server listening on port {}'.format(args['web_server'][0]))
    if sys.version_info>=(3,0):
        WebTrisurf.WebServer(port=args['web_server'][0])
    else:
        print("Cannot start WebServer in python 2.7")
    exit(0)
 
 
def analyze(args,host,analysis,hosts):
    if len(trisurf._analysis_list)==0:
        print ('Error: no decorated function with @analysis decorator!')
        exit(1)
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        target_runs=list(range(1,len(host['runs'])+1))
    for i in target_runs:
 
        for anal in analysis:
            if(anal not in trisurf._analysis_list):
                print("Analysis '"+anal+"' is not known. Available analyses: "+", ".join(trisurf._analysis_list.keys())+".")
                exit(0)
        for anal in analysis:
            retval=trisurf._analysis_list[anal](host['runs'][i-1],host=host, args=args, hosts=hosts)
            #try:
            if(retval):
                exit(0)
            #except:
            #    pass
 
 
    
 
def perform_action(args,host,**kwargs):
    #find which flags have been used and act upon them. -r -s -k -v -c --delete-comment are mutually exclusive, so only one of them is active
    if args['run']:
        run_processes(args,host)
    elif args['kill']:
        kill_processes(args,host)
    elif args['status']:
        status_processes(args,host)
    elif args['comment']!= None:
        comment_processes(args,host)
    elif args['delete_comment']:
        delete_comments(args,host)
    elif args['web_server']!=None:
        start_web_server(args,host)
    elif args['preview']:
        preview_vtu(args,host)
    elif args['jump_to_ipython']:
        print('Jumping to shell...')
        embed()
        exit(0)
    elif args['analysis']!= None:
        analyze(args,host, args['analysis'],kwargs.get('hosts',None))
        exit(0)
    else: #version requested
        print(getTrisurfVersion())
    return
 
 
 
def preview_vtu(args,host):
    from . import VTKRendering
    target_runs=getTargetRunIdxList(args)
    if target_runs==None:
        target_runs=list(range(1,len(host['runs'])+1))
    if host['name'] == socket.gethostname():
        for i in target_runs:
            VTKRendering.Renderer(args,host,host['runs'][i-1])
    else:
        print("VTK rendering currently works on localhost only!")
        
 
def getListOfHostConfigurationByHostname(hosts,host):
    rhost=[]
    for chost in hosts:
        if chost['name'] in host:
            rhost.append(chost)
    return rhost
 
 
 
def start(hosts,argv=sys.argv[1:]):
    args=vars(ParseCLIArguments(argv))
    #Backward compatibility... If running just on localmode, the host specification is unnecessary. Check if only Runs are specified
    try:
        test_host=hosts[0]['name']
    except:
        print("Old syntax detected.")
        hosts=({'name':socket.gethostname(),'address':'127.0.0.1', 'runs':hosts},)
 
    #find the host at which the action is attended
    if args['host']==None:
        #Only status and version commands are automatically executed on all the hosts. stopping or starting  or other actions is not!
        if(args['status']==False and args['version']==False):
            hosts=getListOfHostConfigurationByHostname(hosts,socket.gethostname())
    else:
        hosts=getListOfHostConfigurationByHostname(hosts,args['host'])
    if len(hosts)==0:
        print ('Hostname "{}" does not exist in configuration file. Please check the spelling'.format(args['host'][0]))
        exit(1)
    if not args['local_only']:
            hosts=copyConfigAndConnect(hosts)
    #do local stuff:
    for host in hosts:
        if host['name'] == socket.gethostname():
            if(args['html']):
                print("Host <font color='orange'>"+host['name']+"</font> reports:")
            else:
                print("Host "+bcolors.WARNING+host['name']+bcolors.ENDC+" reports:")
            perform_action(args,host, hosts=hosts)
        elif not args['local_only']:
            if('remotebasepath' in host):
                remote_dir=host['remotebasepath']
            else:
                remote_dir='trisurf_simulations'
            #output=host['_conn'].execute('cd '+remote_dir)
            #print(remote_dir)
            #print(main.__file__)
            #print('python3 '+main.__file__+' -x '+" ".join(argv))
            output=host['_conn'].execute('cd '+remote_dir+ '; python3 '+main.__file__+' -x --originating-host ' +socket.gethostname()+" "+" ".join(argv))
            for line in output:
                print(line.replace('\n',''))
 
 
    if not args['local_only']:
        print("Closing connections to remote hosts")
        for host in hosts:
            if(host['name'] !=socket.gethostname()):
                host['_conn'].disconnect()