opensipscli 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ from .version import __version__
opensipscli/args.py ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ """
21
+ Class that instruct the default values for arguments
22
+ """
23
+
24
+ from opensipscli import defaults
25
+
26
+ class OpenSIPSCLIArgs:
27
+
28
+ """
29
+ Class that contains the default values of CLI Arguments
30
+ """
31
+ debug = False
32
+ print = False
33
+ execute = True
34
+ command = []
35
+ config = None
36
+ instance = defaults.DEFAULT_SECTION
37
+ extra_options = {}
38
+
39
+ __fields__ = ['debug',
40
+ 'print',
41
+ 'execute',
42
+ 'command',
43
+ 'config',
44
+ 'instance',
45
+ 'extra_options']
46
+
47
+ def __init__(self, **kwargs):
48
+ for k in kwargs:
49
+ if k in self.__fields__:
50
+ self.__setattr__(k, kwargs[k])
51
+ else:
52
+ self.extra_options[k] = kwargs[k]
53
+
54
+
55
+ # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
56
+
opensipscli/cli.py ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ import cmd
21
+ import sys
22
+ import os
23
+ import shlex
24
+ import readline
25
+ import atexit
26
+ import importlib
27
+ from opensipscli import args
28
+ from opensipscli import comm
29
+ from opensipscli import defaults
30
+ from opensipscli.config import cfg
31
+ from opensipscli.logger import logger
32
+ from opensipscli.modules import *
33
+
34
+ class OpenSIPSCLI(cmd.Cmd, object):
35
+ """
36
+ OpenSIPS-Cli shell
37
+ """
38
+ modules = {}
39
+ excluded_errs = {}
40
+ registered_atexit = False
41
+
42
+ def __init__(self, options = None):
43
+ """
44
+ contructor for OpenSIPS-Cli
45
+ """
46
+
47
+ if not options:
48
+ options = args.OpenSIPSCLIArgs()
49
+
50
+ self.debug = options.debug
51
+ self.print = options.print
52
+ self.execute = options.execute
53
+ self.command = options.command
54
+ self.modules_dir_inserted = None
55
+
56
+ if self.debug:
57
+ logger.setLevel("DEBUG")
58
+
59
+ cfg_file = None
60
+ if not options.config:
61
+ for f in defaults.CFG_PATHS:
62
+ if os.path.isfile(f) and os.access(f, os.R_OK):
63
+ # found a valid config file
64
+ cfg_file = f
65
+ break
66
+ else:
67
+ cfg_file = options.config
68
+ if not cfg_file:
69
+ logger.debug("no config file found in any of {}".
70
+ format(", ".join(defaults.CFG_PATHS)))
71
+ else:
72
+ logger.debug("using config file {}".format(cfg_file))
73
+
74
+ # __init__ of the configuration file
75
+ cfg.parse(cfg_file)
76
+ if not cfg.has_instance(options.instance):
77
+ logger.warning("Unknown instance '{}'! Using default instance '{}'!".
78
+ format(options.instance, defaults.DEFAULT_SECTION))
79
+ instance = defaults.DEFAULT_SECTION
80
+ else:
81
+ instance = options.instance
82
+ cfg.set_instance(instance)
83
+ if options:
84
+ cfg.set_custom_options(options.extra_options)
85
+
86
+ if not self.execute:
87
+ # __init__ of cmd.Cmd module
88
+ cmd.Cmd.__init__(self)
89
+
90
+ # Opening the current working instance
91
+ self.update_instance(cfg.current_instance)
92
+
93
+ if self.print:
94
+ logger.info(f"Config:\n" + "\n".join([f"{k}: {v}" for k, v in cfg.to_dict().items()]))
95
+
96
+ def update_logger(self):
97
+ """
98
+ alter logging level
99
+ """
100
+
101
+ # first of all, let's handle logging
102
+ if self.debug:
103
+ level = "DEBUG"
104
+ else:
105
+ level = cfg.get("log_level")
106
+ logger.setLevel(level)
107
+
108
+ def clear_instance(self):
109
+ """
110
+ update history
111
+ """
112
+ # make sure we dump everything before swapping files
113
+ self.history_write()
114
+
115
+ def update_instance(self, instance):
116
+ """
117
+ constructor of an OpenSIPS-Cli instance
118
+ """
119
+
120
+ # first of all, let's handle logging
121
+ self.current_instance = instance
122
+ self.update_logger()
123
+
124
+ # Update the intro and prompt
125
+ self.intro = cfg.get('prompt_intro')
126
+ self.prompt = '(%s): ' % cfg.get('prompt_name')
127
+
128
+ # initialize communcation handler
129
+ self.handler = comm.initialize()
130
+
131
+ # remove all loaded modules
132
+ self.modules = {}
133
+
134
+ skip_modules = []
135
+ if cfg.exists('skip_modules'):
136
+ skip_modules = cfg.get('skip_modules')
137
+ sys_modules = {}
138
+ if not self.execute:
139
+ print(self.intro)
140
+ # add the built-in modules and commands list
141
+ for mod in ['set', 'clear', 'help', 'history', 'exit', 'quit']:
142
+ self.modules[mod] = (self, None)
143
+ sys_modules = sys.modules
144
+ else:
145
+ try:
146
+ mod = "opensipscli.modules.{}".format(self.command[0])
147
+ sys_modules = { mod: sys.modules[mod] }
148
+ except:
149
+ pass
150
+
151
+ available_modules = { key[20:]: sys_modules[key] for key in
152
+ sys_modules.keys() if
153
+ key.startswith("opensipscli.modules.") and
154
+ key[20:] not in skip_modules }
155
+ for name, module in available_modules.items():
156
+ m = importlib.import_module("opensipscli.modules.{}".format(name))
157
+ if not hasattr(m, "Module"):
158
+ logger.debug("Skipping module '{}' - does not extend Module".
159
+ format(name))
160
+ continue
161
+ if not hasattr(m, name):
162
+ logger.debug("Skipping module '{}' - module implementation not found".
163
+ format(name))
164
+ continue
165
+ mod = getattr(module, name)
166
+ if not hasattr(mod, '__exclude__') or not hasattr(mod, '__get_methods__'):
167
+ logger.debug("Skipping module '{}' - module does not implement Module".
168
+ format(name))
169
+ continue
170
+ excl_mod = mod.__exclude__(mod)
171
+ if excl_mod[0] is True:
172
+ if excl_mod[1]:
173
+ self.excluded_errs[name] = excl_mod[1]
174
+ logger.debug("Skipping module '{}' - excluded on purpose".format(name))
175
+ continue
176
+ logger.debug("Loaded module '{}'".format(name))
177
+ imod = mod()
178
+ self.modules[name] = (imod, mod.__get_methods__(imod))
179
+
180
+ def history_write(self):
181
+ """
182
+ save history file
183
+ """
184
+ history_file = cfg.get('history_file')
185
+ logger.debug("saving history in {}".format(history_file))
186
+ os.makedirs(os.path.expanduser(os.path.dirname(history_file)), exist_ok=True)
187
+ try:
188
+ readline.write_history_file(os.path.expanduser(history_file))
189
+ except PermissionError:
190
+ logger.warning("failed to write CLI history to {} " +
191
+ "(no permission)".format(
192
+ history_file))
193
+
194
+ def preloop(self):
195
+ """
196
+ preload a history file
197
+ """
198
+ history_file = cfg.get('history_file')
199
+ logger.debug("using history file {}".format(history_file))
200
+ try:
201
+ readline.read_history_file(os.path.expanduser(history_file))
202
+ except PermissionError:
203
+ logger.warning("failed to read CLI history from {} " +
204
+ "(no permission)".format(
205
+ history_file))
206
+ except FileNotFoundError:
207
+ pass
208
+
209
+ readline.set_history_length(int(cfg.get('history_file_size')))
210
+ if not self.registered_atexit:
211
+ atexit.register(self.history_write)
212
+
213
+ def postcmd(self, stop, line):
214
+ """
215
+ post command after switching instance
216
+ """
217
+ if self.current_instance != cfg.current_instance:
218
+ self.clear_instance()
219
+ self.update_instance(cfg.current_instance)
220
+ # make sure we update all the history information
221
+ self.preloop()
222
+
223
+ return stop
224
+
225
+ def print_topics(self, header, cmds, cmdlen, maxcol):
226
+ """
227
+ print topics, omit misc commands
228
+ """
229
+ if header is not None:
230
+ if cmds:
231
+ self.stdout.write('%s\n' % str(header))
232
+ if self.ruler:
233
+ self.stdout.write('%s\n' % str(self.ruler*len(header)))
234
+ self.columnize(cmds, maxcol-1)
235
+ self.stdout.write('\n')
236
+
237
+ def cmdloop(self, intro=None):
238
+ """
239
+ command loop, catching SIGINT
240
+ """
241
+ if self.execute:
242
+ if len(self.command) < 1:
243
+ logger.error("no modules to run specified!")
244
+ return -1
245
+
246
+ module, command, modifiers, params = self.parse_command(self.command)
247
+
248
+ logger.debug("running in non-interactive mode {} {} {}".
249
+ format(module, command, params))
250
+ try:
251
+ ret = self.run_command(module, command, modifiers, params)
252
+ except KeyboardInterrupt:
253
+ print('^C')
254
+ return -1
255
+
256
+ # assume that by default it exists with success
257
+ if ret is None:
258
+ ret = 0
259
+ return ret
260
+ while True:
261
+ try:
262
+ super(OpenSIPSCLI, self).cmdloop(intro='')
263
+ break
264
+ except KeyboardInterrupt:
265
+ print('^C')
266
+ # any other commands exits with negative value
267
+ return -1
268
+
269
+ def emptyline(self):
270
+ if cfg.getBool('prompt_emptyline_repeat_cmd'):
271
+ super().emptyline()
272
+
273
+ def complete_modules(self, text):
274
+ """
275
+ complete modules selection based on given text
276
+ """
277
+ l = [a for a in self.modules.keys() if a.startswith(text)]
278
+ if len(l) == 1:
279
+ l[0] = l[0] + " "
280
+ return l
281
+
282
+ def complete_functions(self, module, text, line, begidx, endidx):
283
+ """
284
+ complete function selection based on given text
285
+ """
286
+
287
+ # builtin commands
288
+ _, command, modifiers, params = self.parse_command(line.split())
289
+ # get all the available modifiers of the module
290
+ all_params = []
291
+ if not command:
292
+ # haven't got to a command yet, so we might have some modifiers
293
+ try:
294
+ modiffunc = getattr(module[0], '__get_modifiers__')
295
+ modifiers_params = modiffunc()
296
+ except:
297
+ pass
298
+ all_params = [ x for x in modifiers_params if x not in modifiers ]
299
+ # if we are introducing a modifier, auto-complete only them
300
+ if begidx > 1 and line[begidx-1] == '-':
301
+ stripped_params = [ p.lstrip("-") for p in modifiers_params ]
302
+ l = [a for a in stripped_params if a.startswith(text)]
303
+ if len(l) == 1:
304
+ l[0] = l[0] + " "
305
+ else:
306
+ l = [a for a in l if a not in [ m.strip("-") for m in modifiers]]
307
+ return l
308
+
309
+ if module[1]:
310
+ all_params = all_params + module[1]
311
+ if len(all_params) > 0 and (not command or
312
+ (len(params) == 0 and line[-1] != ' ')):
313
+ l = [a for a in all_params if a.startswith(text)]
314
+ if len(l) == 1:
315
+ l[0] += " "
316
+ else:
317
+ try:
318
+ compfunc = getattr(module[0], '__complete__')
319
+ l = compfunc(command, text, line, begidx, endidx)
320
+ if not l:
321
+ return None
322
+ except AttributeError:
323
+ return ['']
324
+ # looking for a different command
325
+ return l
326
+
327
+ # Overwritten function for our customized auto-complete
328
+ def complete(self, text, state):
329
+ """
330
+ auto-complete selection based on given text and state parameters
331
+ """
332
+ if state == 0:
333
+ origline = readline.get_line_buffer()
334
+ line = origline.lstrip()
335
+ stripped = len(origline) - len(line)
336
+ begidx = readline.get_begidx() - stripped
337
+ endidx = readline.get_endidx() - stripped
338
+ if begidx > 0:
339
+ mod, args, foo = self.parseline(line)
340
+ if mod == '':
341
+ return self.complete_modules(text)[state]
342
+ elif not mod in self.modules:
343
+ logger.error("BUG: mod '{}' not found!".format(mod))
344
+ else:
345
+ module = self.modules[mod]
346
+ self.completion_matches = \
347
+ self.complete_functions(module, text, line, begidx, endidx)
348
+ else:
349
+ self.completion_matches = self.complete_modules(text)
350
+ try:
351
+ return self.completion_matches[state]
352
+ except IndexError:
353
+ return ['']
354
+
355
+ # Parse parameters
356
+ def parse_command(self, line):
357
+
358
+ module = line[0]
359
+ if len(line) < 2:
360
+ return module, None, [], []
361
+ paramIndex = 1
362
+ while paramIndex < len(line):
363
+ if line[paramIndex][0] != "-":
364
+ break
365
+ paramIndex = paramIndex + 1
366
+ if paramIndex == 1:
367
+ modifiers = []
368
+ command = line[1]
369
+ params = line[2:]
370
+ elif paramIndex == len(line):
371
+ modifiers = line[1:paramIndex]
372
+ command = None
373
+ params = []
374
+ else:
375
+ modifiers = line[1:paramIndex]
376
+ command = line[paramIndex]
377
+ params = line[paramIndex + 1:]
378
+
379
+ return module, command, modifiers, params
380
+
381
+ # Execute commands from Modules
382
+ def run_command(self, module, cmd, modifiers, params):
383
+ """
384
+ run a module command with given parameters
385
+ """
386
+ try:
387
+ mod = self.modules[module]
388
+ except (AttributeError, KeyError):
389
+ if module in self.excluded_errs:
390
+ for err_msg in self.excluded_errs[module]:
391
+ logger.error(err_msg)
392
+ return -1
393
+ else:
394
+ logger.error("no module '{}' loaded".format(module))
395
+ return -1
396
+ # if the module does not return any methods (returned None)
397
+ # we simply call the module's name method
398
+ if not mod[1]:
399
+ if cmd and params is not None:
400
+ params.insert(0, cmd)
401
+ cmd = mod[0].__module__
402
+ if cmd.startswith("opensipscli.modules."):
403
+ cmd = cmd[20:]
404
+ elif not cmd and '' not in mod[1]:
405
+ logger.error("module '{}' expects the following commands: {}".
406
+ format(module, ", ".join(mod[1])))
407
+ return -1
408
+ elif cmd and not cmd in mod[1]:
409
+ logger.error("no command '{}' in module '{}'".
410
+ format(cmd, module))
411
+ return -1
412
+ logger.debug("running command '{}' '{}'".format(cmd, params))
413
+ return mod[0].__invoke__(cmd, params, modifiers)
414
+
415
+ def default(self, line):
416
+ try:
417
+ aux = shlex.split(line)
418
+ except ValueError:
419
+ """ if the line ends in a backspace, just clean it"""
420
+ line = line[:-1]
421
+ aux = shlex.split(line)
422
+
423
+ module, cmd, modifiers, params = self.parse_command(aux)
424
+ self.run_command(module, cmd, modifiers, params)
425
+
426
+ def do_history(self, line):
427
+ """
428
+ print entries in history file
429
+ """
430
+ if not line:
431
+ try:
432
+ with open(os.path.expanduser(cfg.get('history_file'))) as hf:
433
+ for num, line in enumerate(hf, 1):
434
+ print(num, line, end='')
435
+ except FileNotFoundError:
436
+ pass
437
+
438
+ def do_set(self, line):
439
+ """
440
+ handle dynamic settings (key-value pairs)
441
+ """
442
+ parsed = line.split('=', 1)
443
+ if len(parsed) < 2:
444
+ logger.error("setting value format is 'key=value'!")
445
+ return
446
+ key = parsed[0]
447
+ value = parsed[1]
448
+ cfg.set(key, value)
449
+
450
+ # Used to get info for a certain command
451
+ def do_help(self, line):
452
+ # TODO: Add help for commands
453
+ print("Usage:: help cmd - returns information about \"cmd\"")
454
+
455
+ # Clear the terminal screen
456
+ def do_clear(self, line):
457
+ os.system('clear')
458
+
459
+ # Commands used to exit the shell
460
+ def do_EOF(self, line): # It catches Ctrl+D
461
+ print('^D')
462
+ return True
463
+
464
+ def do_quit(self, line):
465
+ return True
466
+
467
+ def do_exit(self, line):
468
+ return True
469
+
470
+ def mi(self, cmd, params = [], silent = False):
471
+ """helper for running MI commands"""
472
+ return comm.execute(cmd, params, silent)
opensipscli/comm.py ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ from opensipscli.logger import logger
21
+ from opensipscli.config import cfg
22
+ from opensips.mi import OpenSIPSMI, OpenSIPSMIException
23
+
24
+ comm_handler = None
25
+ comm_handler_valid = None
26
+
27
+ def initialize():
28
+ global comm_handler
29
+ comm_type = cfg.get('communication_type')
30
+ comm_handler = OpenSIPSMI(comm_type, **cfg.to_dict())
31
+ valid()
32
+
33
+ def execute(cmd, params=[], silent=False):
34
+ global comm_handler
35
+ try:
36
+ ret = comm_handler.execute(cmd, params)
37
+ except OpenSIPSMIException as ex:
38
+ if not silent:
39
+ logger.error("command '{}' returned: {}".format(cmd, ex))
40
+ return None
41
+ return ret
42
+
43
+ def valid():
44
+ global comm_handler
45
+ global comm_handler_valid
46
+ if comm_handler_valid:
47
+ return comm_handler_valid
48
+ if not comm_handler:
49
+ comm_handler_valid = (False, None)
50
+ try:
51
+ if hasattr(comm_handler, "valid"):
52
+ comm_handler_valid = comm_handler.valid()
53
+ else:
54
+ comm_handler_valid = (True, None)
55
+ except:
56
+ comm_handler_valid = (False, None)
57
+ return comm_handler_valid