Advertisement
Guest User

Untitled

a guest
Feb 22nd, 2017
69
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.76 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. import copy
  4. import json
  5. import os
  6. import pprint
  7. import re
  8. import subprocess
  9. import sys
  10. import time
  11.  
  12. import click
  13.  
  14. GS_CUSTOM = 'custom'
  15. GS_MC = 'minecraft'
  16.  
  17. GS_TYPES = [GS_CUSTOM, GS_MC]
  18.  
  19. # overridable default configs to use if no CLI options or config exists
  20. DEFAULT_CONFIGS = {
  21.     'name': 'gameserver',
  22.     'user': 'root',
  23.     'delay_start': 3,
  24.     'delay_stop': 10,
  25.     'delay_prestop': 30,
  26.     'delay_restart': 3,
  27.     'type': GS_CUSTOM,
  28.     'history': 1024
  29. }
  30.  
  31.  
  32. @click.group(chain=True)
  33. @click.option('-c', '--config',
  34.               type=click.Path(),
  35.               help=('Path to JSON config file. Config file options override '
  36.                     'CLI ones. Ignored if file does not exist.'),
  37.               default='.gs_config.json')
  38. @click.option('-s', '--save',
  39.               is_flag=True,
  40.               help=('Save config to JSON file after loading'))
  41. @click.option('-n', '--name',
  42.               type=str,
  43.               help='Name of gameserver screen service, must be unique')
  44. @click.option('-u', '--user',
  45.               type=str,
  46.               help='User to run gameserver as')
  47. @click.option('-ds', '--delay_start',
  48.               type=int,
  49.               help=('Time (in seconds) to wait after service has started to '
  50.                     'verify'))
  51. @click.option('-dt', '--delay_stop',
  52.               type=int,
  53.               help=('Time (in seconds) to wait after service has stopped to '
  54.                     'verify'))
  55. @click.option('-dp', '--delay_prestop',
  56.               type=int,
  57.               help=('Time (in seconds) before stopping the server to allow '
  58.                     'notifing users.'),)
  59. @click.option('-dr', '--delay_restart',
  60.               type=int,
  61.               help=('Time (in seconds) to wait after service has stop before '
  62.                     'restarting'))
  63. @click.option('-t', '--gs_type',
  64.               type=click.Choice(GS_TYPES),
  65.               help='Type of gameserver to run')
  66. @click.option('-c', '--command',
  67.               type=str,
  68.               help='Start up command. Only for Custom server type')
  69. @click.option('-sc', '--stop_command',
  70.               type=str,
  71.               help='Command to stop server. Only for Custom server type')
  72. @click.option('-yc', '--say_command',
  73.               type=str,
  74.               help='Command format to send broadcast to sever. Only for Custom server type')
  75. @click.option('-h', '--history',
  76.               type=int,
  77.               help='Number of lines to show in screen for history')
  78. @click.option('-p', '--path',
  79.               type=click.Path(),
  80.               help='Starting directory. If empty, it uses current directory')
  81. @click.option('-b', '--backup_dir',
  82.               type=click.Path(),
  83.               help='Directory to backup server to. If empty, backups disabled')
  84. @click.option('-d', '--debug',
  85.               is_flag=True)
  86. @click.pass_context
  87. def cli(context, config, save, name, user,
  88.         delay_start, delay_stop, delay_prestop, delay_restart,
  89.         gs_type, command, stop_command, say_command,
  90.         history, path, backup_dir,
  91.         debug):
  92.  
  93.     context.obj['debug'] = debug
  94.     dprint('config init')
  95.  
  96.     # load defaults
  97.     context.obj.update(DEFAULT_CONFIGS)
  98.  
  99.     load_local_config(**locals())
  100.  
  101.     # initalize CLI variables
  102.     context.obj['debug'] = context.obj['debug'] or debug
  103.  
  104.     load_cli_config(**locals())
  105.  
  106.     verify_config()
  107.  
  108.     # save configs back to JSON file
  109.     dprint('final config:')
  110.     dprint(context.obj)
  111.     if save:
  112.         save_local_config()
  113.  
  114.  
  115. @cli.command()
  116. @click.option('-n', '--no_wait',
  117.               is_flag=True)
  118. @click.pass_context
  119. def start(context, no_wait):
  120.     """ starts gameserver """
  121.     dprint('start begin')
  122.  
  123.     if is_running():
  124.         click.secho('{} is already running'
  125.                     .format(context.obj['name']), fg='red')
  126.         context.exit(1)
  127.     else:
  128.         click.echo('starting {}...'.format(context.obj['name']))
  129.         as_user('cd {} && screen -h {} -dmS {} {}'
  130.                 .format(context.obj['path'], context.obj['history'],
  131.                         context.obj['name'], get_command()))
  132.         if not no_wait:
  133.             click.echo('waiting {} seconds before checking if {} is running...'
  134.                        .format(context.obj['delay_start'], context.obj['name']))
  135.             time.sleep(context.obj['delay_start'])
  136.  
  137.         if is_running():
  138.             click.secho('{} is running'.format(context.obj['name']), fg='green')
  139.         else:
  140.             click.secho('could not start {}'.format(context.obj['name']), fg='red')
  141.             context.exit(1)
  142.  
  143.  
  144. @cli.command()
  145. @click.option('-f', '--force',
  146.               is_flag=True)
  147. @click.pass_context
  148. def stop(context, force):
  149.     """ stop gameserver """
  150.     dprint('stop begin')
  151.  
  152.     if is_running():
  153.         if context.obj['delay_prestop'] > 0 and not force:
  154.             click.echo('notifiying users {} is stopping in {} seconds...'
  155.                        .format(context.obj['name'], context.obj['delay_prestop']))
  156.             context.invoke(say, message='server is shutting down in {} seconds...'
  157.                                         .format(context.obj['delay_prestop']))
  158.             time.sleep(context.obj['delay_prestop'])
  159.         click.echo('stopping {}...'.format(context.obj['name']))
  160.  
  161.         stop_command = ''
  162.         if context.obj['type'] == GS_MC:
  163.             stop_command = 'stop'
  164.         else:
  165.             stop_command = context.obj['stop_command']
  166.  
  167.         if force:
  168.             kill_server()
  169.         else:
  170.             context.invoke(command, command_string=stop_command, do_print=False)
  171.             click.echo('waiting {} seconds before checking if {} is stopped...'
  172.                        .format(context.obj['delay_stop'], context.obj['name']))
  173.             time.sleep(context.obj['delay_stop'])
  174.  
  175.         if is_running():
  176.             click.secho('{} could not be stopped'.format(context.obj['name'], fg='red'))
  177.             context.exit(1)
  178.         else:
  179.             click.secho('{} was stopped'.format(context.obj['name'], fg='green'))
  180.     else:
  181.         click.secho('{} is not running'
  182.                     .format(context.obj['name']), fg='red')
  183.         context.exit(1)
  184.  
  185.  
  186. @cli.command()
  187. @click.pass_context
  188. def restart(context):
  189.     """ restarts gameserver"""
  190.     dprint('restart begin')
  191.  
  192.     if is_running():
  193.         context.forward(stop)
  194.     context.forward(start)
  195.  
  196.  
  197. @cli.command()
  198. @click.pass_context
  199. def status(context):
  200.     """ checks if gameserver is runing or not """
  201.     if is_running():
  202.         click.secho('{} is running'.format(context.obj['name']), fg='green')
  203.     else:
  204.         click.secho('{} is not running'.format(context.obj['name']), fg='red')
  205.  
  206.  
  207. @cli.command()
  208. @click.argument('command_string')
  209. @click.pass_context
  210. def command(context, command_string, do_print=True):
  211.     """ runs console command """
  212.     dprint('command begin')
  213.  
  214.     command_string = "screen -p 0 -S {} -X eval 'stuff \"{}\"\015'" \
  215.         .format(context.obj['name'], command_string)
  216.     output = as_user(command_string)
  217.  
  218.     if do_print:
  219.         click.echo(output)
  220.     return output
  221.  
  222.  
  223. @cli.command()
  224. @click.argument('message')
  225. @click.pass_context
  226. def say(context, message):
  227.     """ broadcasts a message to gameserver """
  228.     dprint('say begin')
  229.  
  230.     command_format = '{}'
  231.     if context.obj['type'] == GS_MC:
  232.         command_format = 'say {}'
  233.     else:
  234.         command_format = context.obj['say_command']
  235.  
  236.     context.invoke(command, command_string=command_format.format(message))
  237.  
  238.  
  239. @cli.command()
  240. @click.pass_context
  241. def attach(context):
  242.     """ attachs to gameserver screen """
  243.     if is_running():
  244.         as_user('screen -x {}'.format(context.obj['name']))
  245.     else:
  246.         click.secho('{} is not running'.format(context.obj['name']), fg='red')
  247.         context.exit(1)
  248.  
  249.  
  250. def load_local_config(context, path, config, **kwargs):
  251.     """ verifys working path and loads local config settings """
  252.     # verify working path
  253.     if path is not None:
  254.         if os.path.isdir(path):
  255.             context.obj['path'] = os.path.abspath(path)
  256.         else:
  257.             raise click.BadParameter('path does not exist')
  258.     else:
  259.         path = os.getcwd()
  260.         loop_count = 0
  261.         found = False
  262.         while not found and (path != '/' and loop_count < 5):
  263.             dprint('checking path {}'.format(path))
  264.             if os.path.isfile(os.path.join(path, config)):
  265.                 context.obj['path'] = path
  266.                 found = True
  267.             loop_count += 1
  268.             path = os.path.abspath(os.path.join(path, os.pardir))
  269.  
  270.         if not found:
  271.             context.obj['path'] = os.getcwd()
  272.     dprint('using config path {}'.format(context.obj['path']))
  273.  
  274.     # verify local config file and read it
  275.     config_string = None
  276.     if not os.path.isfile(config):
  277.         config = os.path.join(context.obj['path'], config)
  278.  
  279.     if os.path.isfile(config):
  280.         with open(config, 'r') as config_file:
  281.             config_string = config_file.read().replace('\n', '')
  282.  
  283.     # process config file
  284.     if config_string is not None:
  285.         try:
  286.             configs = json.loads(config_string)
  287.             dprint('local config:')
  288.             dprint(configs)
  289.         except json.JSONDecodeError:
  290.             dprint('invalid configuration file')
  291.         else:
  292.             context.obj.update(configs)
  293.  
  294.  
  295. def load_cli_config(context, name, user,
  296.                     delay_start, delay_stop, delay_prestop, delay_restart,
  297.                     history, gs_type, command, stop_command, say_command, backup_dir,
  298.                     **kwargs):
  299.     if name is not None:
  300.         context.obj['name'] = name
  301.     if user is not None:
  302.         context.obj['user'] = user
  303.     if delay_start is not None:
  304.         context.obj['delay_start'] = delay_start
  305.     if delay_stop is not None:
  306.         context.obj['delay_stop'] = delay_stop
  307.     if delay_prestop is not None:
  308.         context.obj['delay_prestop'] = delay_prestart
  309.     if delay_restart is not None:
  310.         context.obj['delay_restart'] = delay_restart
  311.     if history is not None:
  312.         context.obj['history'] = history
  313.     if gs_type is not None:
  314.         context.obj['type'] = gs_type
  315.     if command is not None:
  316.         context.obj['command'] = command
  317.     if stop_command is not None:
  318.         context.obj['stop_command'] = stop_command
  319.     if say_command is not None:
  320.         context.obj['say_command'] = say_command
  321.     if backup_dir is not None:
  322.         context.obj['backup_dir'] = backup_dir
  323.  
  324.  
  325. def verify_config():
  326.     """ verfies the context config """
  327.     context = click.get_current_context()
  328.  
  329.     # verify backup path
  330.     if context.obj.get('backup_dir') is not None:
  331.         if os.path.isdir(context.obj['backup_dir']):
  332.             context.obj['backup_dir'] = os.path.abspath(context.obj['backup_dir'])
  333.         elif os.path.join(context.obj['path'], context.obj['backup_dir']):
  334.             context.obj['backup_dir'] = os.path.join(context.obj['path'], context.obj['backup_dir'])
  335.         else:
  336.             raise click.BadParameter('backup_dir does not exist')
  337.  
  338.     # verify command param
  339.     if context.obj['type'] == GS_CUSTOM:
  340.         if context.obj.get('command') is None:
  341.             raise click.BadParameter('command is required if server type if custom')
  342.         if context.obj.get('stop_command') is None:
  343.             raise click.BadParameter('stop_command is required if server type if custom')
  344.         if context.obj.get('say_command') is None:
  345.             raise click.BadParameter('say_command is required if server type if custom')
  346.  
  347.  
  348. def save_local_config():
  349.     """ saves current context back to local config """
  350.     context = click.get_current_context()
  351.     dprint('saving config...')
  352.     config_copy = copy.deepcopy(context.obj)
  353.     # delete CLI only configs
  354.     del config_copy['path']
  355.     with open(os.path.join(context.obj['path'], config), 'w') as config_file:
  356.         config_file.write(json.dumps(config_copy, sort_keys=True,
  357.                                      indent=4, separators=(',', ': ')))
  358.  
  359.  
  360. def dprint(msg):
  361.     """ prints message out to console if debug is on """
  362.     context = click.get_current_context()
  363.     if context.obj['debug']:
  364.         if isinstance(msg, dict):
  365.             msg = pprint.pformat(msg)
  366.         click.secho(str(msg), fg='cyan')
  367.  
  368.  
  369. def as_user(command):
  370.     """ runs command as configurated user """
  371.     context = click.get_current_context()
  372.     user = subprocess.getoutput('whoami').strip()
  373.  
  374.     if user != context.obj['user']:
  375.         command = 'sudo su - {} -c "{}"' \
  376.             .format(context.obj['user'], command.replace('"', '\\"'))
  377.  
  378.     dprint('running command \'{}\''.format(command))
  379.     output = subprocess.getoutput(command).strip()
  380.     dprint(output)
  381.     return output
  382.  
  383.  
  384. def get_command(base_only=False):
  385.     """ gets main start command for gameserver """
  386.     context = click.get_current_context()
  387.  
  388.     if context.obj['type'] == GS_MC:
  389.         raise Exception('TODO')
  390.     else:
  391.         command = context.obj['command']
  392.         if base_only:
  393.             command = command.split(' ')[0].split('/')[-1]
  394.         return command
  395.  
  396.  
  397. def kill_server():
  398.     """ forcibly kills server process """
  399.     context = click.get_current_context()
  400.     # grab screen name from gameserver name
  401.     screen = as_user('screen -ls | grep {}'
  402.                      .format(context.obj['name'])).strip()
  403.  
  404.     if screen != '':
  405.         pid = re.match('\d+', screen).group()
  406.         as_user('kill -9 {}'.format(pid))
  407.  
  408.  
  409. def is_running():
  410.     """ checks if gameserver is running """
  411.     context = click.get_current_context()
  412.  
  413.     # grab screen name from gameserver name
  414.     screen = as_user('screen -ls | grep {}'
  415.                      .format(context.obj['name'])).strip()
  416.  
  417.     # check the screen exists
  418.     is_running = (screen != '' and screen is not None)
  419.  
  420.     # check the original command is actually running in screen
  421.     if is_running:
  422.         pid = re.match('\d+', screen).group()
  423.         is_running = (is_running and
  424.                       (as_user('ps -el | grep {} | grep \'{}\''
  425.                                .format(pid, get_command(True)[:15])) != ''))
  426.  
  427.     # TODO: Query server directly...
  428.     return is_running
  429.  
  430.  
  431. if __name__ == '__main__':
  432.     cli(obj={})
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement