def apply_config_file(filepath): """Update cfgFile dict w/yaml config file, ignoring missing files.""" try: with open(filepath) as f: cfg.cfgFile.update(yaml.load(f, Loader)) except IOError as e: if e.strerror == 'No such file or directory': pass else: print(c.yellow("Ignoring config file '{}'; IOError: {}".format( filepath, e.strerror)), file=stderr) except TypeError as e: if e.message == "'NoneType' object is not iterable": # This means it's an empty config file pass else: raise except: print(c.red("Fatal error parsing config file '{}'\n".format(filepath)), file=stderr) raise
def main(argparseOptions): global rOpt, appnamePrefix, rClient rOpt = argparseOptions c.enableColor = rOpt.enableColor runningApps = [] timestamps = [] try: with open(os.devnull, 'w') as devnull: subprocess.check_call(['notify-send', '--version'], stdout=devnull) except: print(c.RED("Unable to launch notify-send command!", "We cannot notify you of events without libnotify installed")) sys.exit(2) try: with open(os.path.expanduser(rOpt.configFile)) as f: cfg = yaml.safe_load(f) except: print(c.yellow( "Note: unable to read configFile '{}'; using defaults" .format(rOpt.configFile))) nick = user = passwd = messg = events = None else: nick = cfg.get('nickname', None) user = cfg.get('ravelloUser', None) passwd = cfg.get('ravelloPass', None) messg = cfg.get('unableToLoginAdditionalMsg', None) events = cfg.get('eventsOfInterest', None) if rOpt.kerberos: appnamePrefix = 'k:' + rOpt.kerberos + '__' elif nick: appnamePrefix = 'k:' + nick + '__' else: appnamePrefix = '' lackingCreds = False if not rOpt.ravelloUser: if user: rOpt.ravelloUser = user elif sys.stdout.isatty(): rOpt.ravelloUser = get_username(c.CYAN("Enter Ravello username: "******"Enter Ravello passphrase: ")) else: lackingCreds = True if lackingCreds: cmd = [ 'notify-send', '--urgency', 'critical', "rav-notify missing Ravello credentials!", "You must either populate ~/.ravshello/ravshello.conf, run " + "rav-notify with -u & -p options, or run rav-notify from a " + "terminal so it can prompt you for user/pass.", ] subprocess.check_call(cmd) sys.exit(3) rClient = ravello_sdk.RavelloClient() try: # Try to log in. rClient.login(rOpt.ravelloUser, rOpt.ravelloPass) except: if sys.stdout.isatty(): print(c.RED("Logging in to Ravello failed!")) print("\nRe-check your username and password.") if messg: print(messg) else: cmd = [ 'notify-send', '--urgency', 'critical', "rav-notify failed to log in to Ravello!", "Re-check your username and password.", ] subprocess.check_call(cmd) sys.exit(5) cmd = [ 'notify-send', '--urgency', 'low', "rav-notify monitoring Ravello events", "Any events of interest (app timeouts or deletions, vms being " + "started or stopped) will trigger further notifications", ] subprocess.check_call(cmd) if events: eventsOfInterest = events else: eventsOfInterest = [ 'APP_TIMEOUT_AUTO_STOPPING', 'APP_TIMEOUT_AUTO_STOPPED', 'APPLICATION_TIMER_RESET', 'APPLICATION_DELETED', 'VM_STOPPED', 'VM_STARTED', 'VM_SNAPSHOTTING_AFTER_STOP', 'VM_FINISHED_SNAPSHOTTING', ] debug("Event triggers:\n{}\n".format("\n".join(eventsOfInterest))) urgency = { 'INFO': "low", 'WARN': "normal", 'ERROR': "critical", } # Build a list of app ids we should pay attention to. myAppIds = update_myAppIds() for appId in myAppIds: app = rClient.get_application(appId, aspect='properties') try: # Grab expiration time for all of my deployed apps. expirationTime = app['deployment']['expirationTime'] except: continue else: a = { 'id': appId, 'name': app['name'].replace(appnamePrefix, ''), 'expirationTime': sanitize_timestamp(expirationTime), } runningApps.append(a) # Run forever-loop to watch for notifications or expiring apps. while 1: # Run check to see if any apps are about to expire. act_on_imminent_app_expiration(runningApps) myEvents = [] # Set lower bound to 5 minutes ago, upper bound to right now. # Unusual manipulation present because Ravello expects timestamps to # include thousandths of a sec, but not as floating-point. start = time.time() - (5*60 + rOpt.refreshInterval) start = int("{:.3f}".format(start).replace('.', '')) end = int("{:.3f}".format(time.time()).replace('.', '')) query = { 'dateRange': { 'startTime': start, 'endTime': end, }, } try: # Perform our search. results = rClient.search_notifications(query) except ravello_sdk.RavelloError as e: if e.args[0] == 'request timeout': # Timeout, so try one more time. results = rClient.search_notifications(query) try: # Results are returned in reverse-chronological order. for event in reversed(results['notification']): try: # Only deal with events we have not seen before that relate # to one of myAppIds. if (any(appId == event['appId'] for appId in myAppIds) and event['eventTimeStamp'] not in timestamps): myEvents.append(event) except: pass except: pass # Iterate over events relevant to my apps. for event in myEvents: if any(etype in event['eventType'] for etype in eventsOfInterest): # Get application data if event of interest. try: app = rClient.get_application( event['appId'], aspect='properties') except KeyError: # Will fail if event is not about an app, i.e.: on user login. continue else: continue # Add unique timestamp for this event to our list, to prevent acting # on it in a subsequent loop. timestamps.append(event['eventTimeStamp']) try: appName = app['name'].replace(appnamePrefix, '') except TypeError: # Will fail if app was deleted. appName = '' if event['eventType'] == 'APPLICATION_TIMER_RESET': try: # Grab expiration time if app is deployed. expirationTime = app['deployment']['expirationTime'] except: # (app isn't deployed) pass else: expirationTime = sanitize_timestamp(expirationTime) for a in runningApps: # Try to find the app by id in our existing list. if a['id'] == app['id']: # Update the app's expirationTime timestamp. a['expirationTime'] = expirationTime break else: # If the appId for the APPLICATION_TIMER_RESET event isn't # present in our runningApps list, we need to add it. a = { 'id': app['id'], 'name': appName, 'expirationTime': expirationTime, } runningApps.append(a) else: # Event type is anything but APPLICATION_TIMER_RESET. tstamp = datetime.fromtimestamp( sanitize_timestamp(timestamps[-1]) ).strftime("%H:%M:%S") if appName: appName = " ({})".format(appName) msg = event['eventProperties'][0]['value'].replace(appnamePrefix, '') cmd = [ 'notify-send', '--urgency', urgency[event['notificationLevel']], "{}{}".format(event['eventType'], appName), "[{}] {}".format(tstamp, msg), ] subprocess.check_call(cmd) if rOpt.enableDebug and sys.stdout.isatty(): i = rOpt.refreshInterval while i >= 0: print(c.REVERSE("{}".format(i)), end='') sys.stdout.flush() time.sleep(1) print('\033[2K', end='') i -= 1 print() else: time.sleep(rOpt.refreshInterval) myAppIds = update_myAppIds(myAppIds)
def main(): """Parse cmdline args, configure prefs, login, and start captive UI.""" # Setup parser description = ("Interface with Ravello Systems to create & manage apps " "hosted around the world") epilog = ( "ENVIRONMENT VARIABLES:\n" " Various printing commands in {} make use of the RAVSH_EDITOR variable\n" " if it is present, falling back to the EDITOR variable. If that's empty, the\n" " fall-back process is to use: gvim, vim, and finally less.\n\n" "VERSION:\n" " {}\n" " Report bugs/RFEs/feedback at https://github.com/ryran/ravshello/issues" .format(cfg.prog, cfg.version)) p = argparse.ArgumentParser( prog=cfg.prog, description=description, add_help=False, epilog=epilog, formatter_class=lambda prog: CustomFormatter(prog)) # Setup groups for help page: grpU = p.add_argument_group('UNIVERSAL OPTIONS') grpA = p.add_argument_group( 'ADMINISTRATIVE FEATURES', description="Require that Ravello account user has admin rights") # Universal opts: grpU.add_argument('-h', '--help', dest='showHelp', action='store_true', help="Show this help message and exit") grpU.add_argument( '-u', '--user', dest='ravelloUser', metavar='USER', default='', help=("Explicitly specify Ravello username or profile name from {} " "config file (will automatically prompt for passphrase if none " "is present in cfgfile)".format(cfg.defaultUserCfgFile))) grpU.add_argument( '-p', '--passwd', dest='ravelloPass', metavar='PASSWD', default='', help=("Explicitly specify a Ravello user password on the command-line " "(unsafe on multi-user system)")) grpU_0 = grpU.add_mutually_exclusive_group() grpU_0.add_argument( '-k', '--nick', dest='nick', help=("Explicitly specify a nickname to use for app-filtering " "(nickname is normally determined from the system user name " "and is used to hide applications that don't start with " "'k:NICK__'; any apps created will also have that tag prefixed " "to their name; see also 'nickname' and 'appnameNickPrefix' " "config directives in /usr/share/{}/config.yaml)".format( cfg.prog))) grpU_0.add_argument('--prompt-nick', dest='promptNickname', action='store_true', help="Prompt for nickname to use for app-filtering") grpU.add_argument( '--never-prompt-creds', dest='neverPromptCreds', action='store_true', help=( "Never prompt for Ravello user or pass credentials if missing; " "instead exit with standard 'Logging in to Ravello failed' message " "and set exit code '5' (note that using this will override an " "explicit 'neverPromptCreds=false' setting from a config file)")) grpU.add_argument('-n', '--nocolor', dest='enableColor', action='store_false', help="Disable all color terminal enhancements") grpU.add_argument('--cfgdir', dest='userCfgDir', metavar='CFGDIR', default=cfg.defaultUserCfgDir, help=("Explicitly specify path to user config directory " "(default: '{}')".format(cfg.defaultUserCfgDir))) grpU.add_argument( '--cfgfile', dest='cfgFileName', metavar='CFGFILE', default=cfg.defaultUserCfgFile, help= ("Explicitly specify basename of optional per-user yaml config file " "containing login credentials & other settings (default: '{default}'); " "note that this will be created in CFGDIR; also note that this " "file will be read AFTER /usr/share/{prog}/config.yaml & " "/etc/{prog}/config.yaml".format(default=cfg.defaultUserCfgFile, prog=cfg.prog))) grpU.add_argument( '--clearprefs', dest='clearPreferences', action='store_true', help="Delete prefs.bin in per-user CFGDIR before starting") grpU.add_argument('-q', '--quiet', dest='enableVerbose', action='store_false', help="Hide verbose messages during startup") grpU.add_argument( '-Q', '--more-quiet', dest='printWelcome', action='store_false', help=( "A superset of the --quiet option; also hides various non-verbose " "welcome messages during startup")) grpU.add_argument( '-d', '--debug', dest='enableDebugging', action='store_true', help=("Turn on debugging features to help troubleshoot a problem " "(critically, this disables some ConfigShell exception-handling " "so that errors in commands will cause {} to exit)".format( cfg.prog))) grpU.add_argument( '-D', '--directsdk', action='store_true', help=( "Replaces the standard {} interface with a shell that provides " "direct access to the Ravello SDK (note that this shell respects " "the --stdin & --scripts options, as well as any cmdline args)". format(cfg.prog))) grpU.add_argument('-V', '--version', action='version', version=cfg.version) # Admin-only opts: grpA.add_argument('-a', '--admin', dest='enableAdminFuncs', action='store_true', help="Enable admin functionality") grpA.add_argument( '-A', '--allapps', dest='showAllApps', action='store_true', help=("Show all applications, including ones not associated with your " "user (automatically triggers --admin option)")) grpA_0 = grpA.add_mutually_exclusive_group() grpA_0.add_argument( '-0', '--stdin', dest='useStdin', action='store_true', help=("Enable reading newline-delimited commands from stdin " "(these commands will be executed instead of entering the " "interactive shell -- automatic exit after last cmd)")) grpA_0.add_argument( '-s', '--script', dest='scriptFile', metavar='FILE', help=("Specify a script file containing newline-delimited " "commands (these commands will be executed instead of entering " "the interactive shell -- automatic exit after last cmd)")) grpA.add_argument( 'cmdlineArgs', metavar='COMMANDS', nargs=argparse.REMAINDER, help=("If any additional cmdline args are present, each shell word " "will be treated as a separate command and they will all be " "executed prior to entering the interactive shell (ensure " "each cmd is quoted to protect from shell expansion!)")) # Build out options namespace cfg.opts = rOpt = p.parse_args() # Halp-quit if rOpt.showHelp: p.print_help() exit() # Trigger -q if -Q was called if not rOpt.printWelcome: rOpt.enableVerbose = False # Setup color/verbosity c.enableColor = rOpt.enableColor c.enableVerbose = rOpt.enableVerbose c.enableDebug = rOpt.enableDebugging # Trigger -a if -A was called if rOpt.showAllApps: rOpt.enableAdminFuncs = True if not rOpt.enableAdminFuncs: if rOpt.cmdlineArgs or rOpt.scriptFile or rOpt.useStdin: print(c.red( "Sorry! Only admins are allowed to use {} non-interactively". format(cfg.prog)), file=stderr) exit(1) if rOpt.directsdk: print(c.red( "Sorry! Only admins are allowed to use the direct SDK shell!"), file=stderr) exit(1) # Print warnings about incompatible options if rOpt.useStdin and rOpt.cmdlineArgs: print( c.yellow("Ignoring cmdline-args because -0/--stdin was requested"), file=stderr) elif rOpt.scriptFile and rOpt.cmdlineArgs: print(c.yellow( "Ignoring cmdline-args because -s/--script was requested"), file=stderr) # Expand userCfgDir in case of tildes; set to default if missing specified dir if os.path.isdir(os.path.expanduser(rOpt.userCfgDir)): rOpt.userCfgDir = os.path.expanduser(rOpt.userCfgDir) else: rOpt.userCfgDir = os.path.expanduser(cfg.defaultUserCfgDir) # Read package config file apply_config_file('/usr/share/{}/config.yaml'.format(cfg.prog)) # Read system config file apply_config_file('/etc/{}/config.yaml'.format(cfg.prog)) # Read user config file apply_config_file(os.path.join(rOpt.userCfgDir, rOpt.cfgFileName)) # Do some checking of cfgfile options if cfg.cfgFile: # Handle include files includes = cfg.cfgFile.get('includes', []) if isinstance(includes, list): # Handle glob-syntax L = [] for filepath in includes: L.extend(glob(os.path.expanduser(filepath))) for filepath in L: apply_config_file(filepath) else: print(c.yellow( "Error: Ignoring configFile `includes` directive because it's not a list\n" " See /usr/share/{}/config.yaml for example".format( cfg.prog)), file=stderr) # Validate pre-run commands preRunCommands = cfg.cfgFile.get('preRunCommands', []) if not isinstance(preRunCommands, list): print(c.yellow( "Error: Ignoring configFile `preRunCommands` directive because it's not a list\n" " See /usr/share/{}/config.yaml for example".format( cfg.prog)), file=stderr) del cfg.cfgFile['preRunCommands'] # Validate neverPromptCreds neverPromptCreds = cfg.cfgFile.get('neverPromptCreds', False) if isinstance(neverPromptCreds, bool): if neverPromptCreds: rOpt.neverPromptCreds = True else: print(c.yellow( "Error: Ignoring configFile `neverPromptCreds` directive because it's not a boolean\n" " See /usr/share/{}/config.yaml for example".format( cfg.prog)), file=stderr) # Set sshKeyFile var to none if missing cfg.cfgFile['sshKeyFile'] = cfg.cfgFile.get('sshKeyFile', None) if rOpt.printWelcome: print(c.BOLD("Welcome to {}!".format(cfg.prog)), file=stderr) # Liftoff # 1.) Establish a local user name to use in ravshello # This name is arbitrary and has nothing to do with Ravello login creds # It is used: # - To construct names for new apps # - To restrict which apps can be seen # - To determine if admin functionality is unlockable (assuming -a or -A) cfg.user = auth_local.authorize_user() # 2.) Use ravello_sdk.RavelloClient() object to log in to Ravello cfg.rClient = auth_ravello.login() cfg.rCache = ravello_cache.RavelloCache(cfg.rClient) # 3.) Launch main configShell user interface # It will read options and objects from the cfg module user_interface.main()