summaryrefslogtreecommitdiffstats
path: root/scripts/master_daemon.py
blob: caaaf5fcbbb6347f27adaf37f4b1d2fcaa951a36 (plain)
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
#!/usr/bin/python
import os, sys, getpass
import tempfile
import argparse
import socket
from time import time, sleep
import subprocess, shlex, shutil, multiprocessing
from distutils.dir_util import mkpath
from glob import glob
import logging
logging.basicConfig(level = logging.FATAL) # keep quiet

sys.path.append(os.path.join('external_libs', 'jsonrpclib-pelix-0.2.5'))
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer

sys.path.append(os.path.join('external_libs', 'termstyle'))
import termstyle


### Server functions ###

def check_connectivity():
    return True

def add(a, b): # for sanity checks
    return a + b

def is_trex_daemon_running():
    ret_code, stdout, stderr = run_command('ps -u root --format comm')
    if ret_code:
        raise Exception('Failed to list running processes, stderr: %s' % stderr)
    if 'trex_daemon_ser' in stdout: # name is cut
        return True
    return False

def restart_trex_daemon():
    if is_trex_daemon_running:
        stop_trex_daemon()
    start_trex_daemon()
    return True

def stop_trex_daemon():
    if not is_trex_daemon_running():
        return False
    return_code, stdout, stderr = run_command('%s stop' % trex_daemon_path)
    if return_code:
        raise Exception('Could not stop trex_daemon_server, err: %s' % stderr)
    for i in range(20):
        if not is_trex_daemon_running():
            return True
        sleep(0.1)
    raise Exception('Could not stop trex_daemon_server')

def start_trex_daemon():
    if is_trex_daemon_running():
        return False
    return_code, stdout, stderr = run_command('%s start' % trex_daemon_path)
    if return_code:
        raise Exception('Could not run trex_daemon_server, err: %s' % stderr)
    for i in range(20):
        if is_trex_daemon_running():
            return True
        sleep(0.1)
    raise Exception('Could not run trex_daemon_server')

def update_trex(package_path = 'http://trex-tgn.cisco.com/trex/release/latest'):
    if not args.allow_update:
        raise Exception('Updading server not allowed')
    try:
        cur_dir = args.trex_dir
        bu_dir = '%s_BU%i' % (cur_dir, int(time()))
        # rename current dir for backup
        try:
            if os.path.exists(bu_dir):
                shutil.rmtree(bu_dir)
            shutil.move(cur_dir, bu_dir)
            mkpath(cur_dir)
        except Exception as e:
            raise Exception('Could not make backup of previous directory. Err: %s' % e)
        # getting new package
        if package_path.startswith('http://'):
            file_name = package_path.split('/')[-1]
            ret_code, stdout, stderr = run_command('wget %s -O %s' % (package_path, cur_dir), timeout = 600)
        elif os.path.normpath(package_path).startswith('/auto/') and package_path.endswith('.tar.gz'):
            file_name = os.path.basename(package_path)
            ret_code, stdout, stderr = run_command('rsync -cL %s %s' % (package_path, cur_dir), timeout = 300)
        else:
            raise Exception('package_path should be http address or path starting with /auto')
        if ret_code:
            raise Exception('Could not get requested package: %s' % stderr)
        # unpacking
        ret_code, stdout, stderr = run_command('tar -mxzf %s -C %s' % (os.path.join(cur_dir, file_name), cur_dir))
        if ret_code:
            raise Exception('Could not untar the package: %s' % stderr)
        # version is the name of dir
        dir_cont = glob(os.path.join(cur_dir, 'v[0-9].[0-9][0-9]'))
        if not len(dir_cont):
            raise Exception('Did not found directory with version name after unpacking.')
        if len(dir_cont) > 1:
            raise Exception('Found more than one directory with version name after unpacking.')
        version = os.path.basename(dir_cont[0])
        # moving files from dir with version one level up
        for root, dirs, files in os.walk(os.path.join(cur_dir, version)):
            for f in files:
                shutil.move(os.path.join(root, f), os.path.join(cur_dir, f))
            for d in dirs:
                shutil.move(os.path.join(root, d), os.path.join(cur_dir, d))
        # no errors, remove BU dir
        if os.path.exists(bu_dir):
            shutil.rmtree(bu_dir)
        return True
    except: # something went wrong, return backup dir
        if os.path.exists(cur_dir):
            shutil.rmtree(cur_dir)
        shutil.move(bu_dir, cur_dir)
        raise


def unpack_client():
    if not args.allow_update:
        raise Exception('Unpacking of client not allowed')
    # determining client archive
    client_pkg_files = glob(os.path.join(args.trex_dir, 'trex_client*.tar.gz'))
    if not len(client_pkg_files):
        raise Exception('Could not find client package')
    if len(client_pkg_files) > 1:
        raise Exception('Found more than one client packages')
    client_pkg_name = os.path.basename(client_pkg_files[0])
    client_new_path = os.path.join(args.trex_dir, 'trex_client')
    if os.path.exists(client_new_path):
        if os.path.islink(client_new_path) or os.path.isfile(client_new_path):
            os.unlink(client_new_path)
        else:
            shutil.rmtree(client_new_path)
    # unpacking
    ret_code, stdout, stderr = run_command('tar -mxzf %s -C %s' % (client_pkg_files[0], args.trex_dir))
    if ret_code:
        raise Exception('Could not untar the client package: %s' % stderr)
    return True

### /Server functions ###

def fail(msg):
    print(msg)
    sys.exit(-1)

def run_command(command, timeout = 15):
    command = 'timeout %s %s' % (timeout, command)
    # pipes might stuck, even with timeout
    with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
        proc = subprocess.Popen(shlex.split(command), stdout=stdout_file, stderr=stderr_file)
        proc.wait()
        stdout_file.seek(0)
        stderr_file.seek(0)
        return (proc.returncode, stdout_file.read().decode(errors = 'replace'), stderr_file.read().decode(errors = 'replace'))

def show_master_daemon_status():
    if get_master_daemon_pid():
        print(termstyle.red('Master daemon is running'))
    else:
        print(termstyle.red('Master daemon is NOT running'))

def start_master_daemon():
    if get_master_daemon_pid():
        print(termstyle.red('Master daemon is already running'))
        return
    server = multiprocessing.Process(target = start_master_daemon_func)
    server.daemon = True
    server.start()
    for i in range(10):
        if get_master_daemon_pid():
            print(termstyle.green('Master daemon is started'))
            os._exit(0)
        sleep(0.1)
    fail(termstyle.red('Master daemon failed to run'))

def restart_master_daemon():
    if get_master_daemon_pid():
        kill_master_daemon()
    start_master_daemon()

def start_master_daemon_func():
    server = SimpleJSONRPCServer(('0.0.0.0', args.daemon_port))
    print('Started master daemon (port %s)' % args.daemon_port)
    server.register_function(add)
    server.register_function(check_connectivity)
    server.register_function(is_trex_daemon_running)
    server.register_function(restart_trex_daemon)
    server.register_function(start_trex_daemon)
    server.register_function(stop_trex_daemon)
    server.register_function(unpack_client)
    server.register_function(update_trex)
    server.serve_forever()


def get_master_daemon_pid():
    return_code, stdout, stderr = run_command('netstat -tlnp')
    if return_code:
        fail('Failed to determine which program holds port %s, netstat error: %s' % (args.daemon_port, stderr))
    for line in stdout.splitlines():
        if '0.0.0.0:%s' % args.daemon_port in line:
            line_arr = line.split()
            if len(line_arr) != 7:
                fail('Could not parse netstat line to determine which process holds port %s: %s'(args.daemon_port, line))
            if '/' not in line_arr[6]:
                fail('Expecting pid/program name in netstat line of using port %s, got: %s'(args.daemon_port, line_arr[6]))
            pid, program = line_arr[6].split('/')
            if 'python' not in program and 'master_daemon' not in program:
                fail('Some other program holds port %s, not our daemon: %s. Please verify.' % (args.daemon_port, program))
            return pid
    return None

def kill_master_daemon():
    pid = get_master_daemon_pid()
    if not pid:
        print(termstyle.red('Master daemon is NOT running'))
        return True
    return_code, stdout, stderr = run_command('kill %s' % pid) # usual kill
    if return_code:
        fail('Failed to kill master daemon, error: %s' % stderr)
    for i in range(10):
        if not get_master_daemon_pid():
            print(termstyle.green('Master daemon is killed'))
            return True
        sleep(0.1)
    return_code, stdout, stderr = run_command('kill -9 %s' % pid) # unconditional kill
    if return_code:
        fail('Failed to kill trex_daemon, error: %s' % stderr)
    for i in range(10):
        if not get_master_daemon_pid():
            print(termstyle.green('Master daemon is killed'))
            return True
        sleep(0.1)
    fail('Failed to kill master daemon, even with -9. Please review manually.') # should not happen

# returns True if given path is under current dir or /tmp
def _check_path_under_current_or_temp(path):
    if not os.path.relpath(path, '/tmp').startswith(os.pardir):
        return True
    if not os.path.relpath(path, os.getcwd()).startswith(os.pardir):
        return True
    return False

### Main ###

if getpass.getuser() != 'root':
    fail('Please run this program as root/with sudo')

actions_help = '''Specify action command to be applied on master daemon.
    (*) start      : start the master daemon.
    (*) show       : prompt the status of master daemon process (running / not running).
    (*) stop       : exit the master daemon process.
    (*) restart    : stop, then start again the master daemon process
    '''
action_funcs = {'start': start_master_daemon,
                'show': show_master_daemon_status,
                'stop': kill_master_daemon,
                'restart': restart_master_daemon,
                }

parser = argparse.ArgumentParser(description = 'Runs master daemon that can start/stop TRex daemon or update TRex version.')
parser.add_argument('-p', '--daemon-port', type=int, default = 8091, dest='daemon_port', 
                    help = 'Select port on which the master_daemon runs.\nDefault is 8091.', action = 'store')
parser.add_argument('-d', '--trex-dir', type=str, default = os.getcwd(), dest='trex_dir',
                    help = 'Path of TRex, default is current dir', action = 'store')
parser.add_argument('--allow-update', default = False, dest='allow_update', action = 'store_true',
                    help = "Allow update of TRex via RPC command. WARNING: It's security hole! Use on your risk!")
parser.add_argument('action', choices=action_funcs.keys(),
                    action='store', help=actions_help)
parser.usage = None
args = parser.parse_args()

if not os.path.exists(args.trex_dir):
    raise Exception("Given path '%s' does not exist" % args.trex_dir)
if not _check_path_under_current_or_temp(args.trex_dir):
    raise Exception('Only allowed to use path under /tmp or current directory')

trex_daemon_path = os.path.join(args.trex_dir, 'trex_daemon_server')
action_funcs[args.action]()