Browse Source

Did some linting.

master
Bob 8 years ago
parent
commit
0e96d429aa
  1. 4
      .flake8
  2. 2
      README.md
  3. 592
      belay.py

4
.flake8

@ -0,0 +1,4 @@
[flake8]
exclude = .git
# 79 character limit is impractical with a 4 space indent...
ignore = E501

2
README.md

@ -1,6 +1,6 @@
# belay # belay
A simple python utility for checking up on your Slack organization. A simple python utility for checking up on your Slack organization. It's sort of like AWS Config Audit, but for a Slack team.
## Setup ## Setup

592
belay.py

@ -1,309 +1,347 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
import sys
import yaml import yaml
import logging import logging
import argparse import argparse
from slackclient import SlackClient from slackclient import SlackClient
program_version="1.0" program_version = "1.0"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_config(conf_path=None, team=None): def load_config(conf_path=None, team=None):
# Try the usual places to store an API token # Try the usual places to store an API token
# First token wins # First token wins
api_token = None api_token = None
bot_token = None bot_token = None
config_data = {} config_data = {}
homedir = os.path.expanduser("~") homedir = os.path.expanduser("~")
scriptdir = os.path.dirname(os.path.realpath(__file__)) scriptdir = os.path.dirname(os.path.realpath(__file__))
# 1. Specified config file # 1. Specified config file
if conf_path: if conf_path:
logger.debug("Skipping standard config locations because config path passed in on the command line.") logger.debug("Skipping standard locations because config path passed on the command line.")
# 2. ~/.config/slack/config.yml # 2. ~/.config/slack/config.yml
elif os.path.isfile(os.path.join(homedir,".config","belay","config.yml")): elif os.path.isfile(os.path.join(homedir, ".config", "belay", "config.yml")):
conf_path = os.path.join(homedir,".config","belay","config.yml") conf_path = os.path.join(homedir, ".config", "belay", "config.yml")
logger.info("Found config file in Home Directory: %s", conf_path) logger.info("Found config file in Home Directory: %s", conf_path)
# 3. ./config.yml # 3. ./config.yml
elif os.path.isfile(os.path.join(scriptdir,"config.yml")): elif os.path.isfile(os.path.join(scriptdir, "config.yml")):
conf_path = os.path.join(scriptdir,"config.yml") conf_path = os.path.join(scriptdir, "config.yml")
logger.info("Found config file in current script directory: %s", conf_path) logger.info("Found config file in current script directory: %s", conf_path)
# OK, now actually pull values from the configs # OK, now actually pull values from the configs
if conf_path: if conf_path:
logger.info("Attempting to load config from file.") logger.info("Attempting to load config from file.")
with open(conf_path, "r") as config: with open(conf_path, "r") as config:
config_data = yaml.load(config, Loader=yaml.loader.BaseLoader) config_data = yaml.load(config, Loader=yaml.loader.BaseLoader)
logger.debug("Loaded config: %s", config_data) logger.debug("Loaded config: %s", config_data)
if "teams" in config_data: if "teams" in config_data:
logger.debug("This is a multi-team config file. Script is looking for team: %s", team) logger.debug("This is a multi-team config file. Script is looking for team: %s", team)
if not team: if not team:
raise ValueError("No team specified with multi-team config. Please indicate which team you'd like to use.") raise ValueError("No team specified with multi-team config. " +
elif team not in config_data["teams"]: "Please indicate which team you'd like to use.")
raise ValueError("Specified team '"+team+"' not present in config file. Valid choices are: "+", ".join(config_data["teams"].keys())) elif team not in config_data["teams"]:
else: raise ValueError("Specified team '" + team +
logger.debug("Team '%s' found. Selecting only data for that team.", team) "' not present in config file. Valid choices are: " +
config_data = config_data["teams"][team] ", ".join(config_data["teams"].keys()))
# Allow people to set keys in the env if they don't want them in files else:
# Clear bot if we find API, to try to avoid mixing teams logger.debug("Team '%s' found. Selecting only data for that team.", team)
if "SLACK_API_TOKEN" in os.environ: config_data = config_data["teams"][team]
config_data.pop("bot_token", 0) # Allow people to set keys in the env if they don't want them in files
config_data["api_token"] = os.environ["SLACK_API_TOKEN"] # Clear bot if we find API, to try to avoid mixing teams
logger.info("Found API token in environment, adding to config from file.") if "SLACK_API_TOKEN" in os.environ:
logger.debug("API Token: %s", api_token) config_data.pop("bot_token", 0)
logger.info("Checking environment for SLACK_BOT_TOKEN variable...") config_data["api_token"] = os.environ["SLACK_API_TOKEN"]
if "SLACK_BOT_TOKEN" in os.environ: logger.info("Found API token in environment, adding to config from file.")
config_data["bot_token"] = os.environ["SLACK_BOT_TOKEN"] logger.debug("API Token: %s", api_token)
logger.info("Found bot token in environment, adding to config from file.") logger.info("Checking environment for SLACK_BOT_TOKEN variable...")
logger.debug("Bot Token: %s", bot_token) if "SLACK_BOT_TOKEN" in os.environ:
config_data["bot_token"] = os.environ["SLACK_BOT_TOKEN"]
logger.info("Found bot token in environment, adding to config from file.")
logger.debug("Bot Token: %s", bot_token)
if "api_token" not in config_data: if "api_token" not in config_data:
raise RuntimeError("No API token not found.") raise RuntimeError("No API token not found.")
if "bot_token" not in config_data: if "bot_token" not in config_data:
logger.warn("No bot token provided. Assuming API token is legacy token. Use of legacy tokens is a potential security issue and we strongly recommend switching to the new token format.") logger.warn("No bot token provided. Assuming API token is legacy token. " +
return config_data "Use of legacy tokens is a potential security issue and we strongly " +
"recommend switching to the new token format.")
return config_data
def belay(config):
slack_api = SlackClient(config["api_token"])
api_test = slack_api.api_call("auth.test")
logger.debug("API Token auth.test results: %s", api_test)
if not api_test["ok"]:
raise ValueError("API Token is invalid.")
api_user = slack_api.api_call("users.info", user=api_test["user_id"])
logger.debug("User info for API token user: %s", api_user)
if api_user["user"]["is_bot"]:
raise ValueError("API Token is a bot token. This will not work.")
if "bot_token" in config:
slack_bot = SlackClient(config["bot_token"])
bot_test = slack_bot.api_call("auth.test")
logger.debug("Bot Token auth.test results: %s", bot_test)
if not bot_test["ok"]:
raise ValueError("Bot Token is invalid.")
bot_user = slack_bot.api_call("users.info", user=bot_test["user_id"])
logger.debug("User info for Bot token user: %s", bot_user)
if not bot_user["user"]["is_bot"]:
raise ValueError("Bot Token does not correspond to a bot user.")
else:
slack_bot = slack_api
integration_issues = check_integrations(slack_api, config)
if integration_issues:
logger.info("Found the following integration issues: %s", integration_issues)
notify_problems(integration_issues, config, slack_bot, "Problem Integrations:", "integration_name", "date")
else:
logger.info("No integration issues found.")
user_issues = check_users(slack_api, config)
if user_issues:
logger.info("Found the following user issues: %s", user_issues)
notify_problems(user_issues, config, slack_bot, "Problem Users:", "name", "name")
else:
logger.info("No user issues found.")
def check_integrations(api_client, config): def belay(config):
if "skip_integrations" in config and config["skip_integrations"]: slack_api = SlackClient(config["api_token"])
return {} api_test = slack_api.api_call("auth.test")
result = api_client.api_call("team.integrationLogs") logger.debug("API Token auth.test results: %s", api_test)
logger.debug("Integration log results: %s", result) if not api_test["ok"]:
if not result["ok"]: raise ValueError("API Token is invalid.")
raise RuntimeError("API Call encountered an error while getting initial integrations: "+unicode(result)) api_user = slack_api.api_call("users.info", user=api_test["user_id"])
iLogs = result["logs"] logger.debug("User info for API token user: %s", api_user)
if result["paging"]["pages"] > 1: if api_user["user"]["is_bot"]:
logger.info("Multiple pages of integration logs exist. Pulling remaining %s pages...", result["paging"]["pages"] - 1) raise ValueError("API Token is a bot token. This will not work.")
while result["paging"]["page"] < result["paging"]["pages"]: if "bot_token" in config:
nextPage = result["paging"]["page"] + 1 slack_bot = SlackClient(config["bot_token"])
logger.info("Pulling page %s.", nextPage) bot_test = slack_bot.api_call("auth.test")
result = api_client.api_call("team.integrationLogs", page=nextPage) logger.debug("Bot Token auth.test results: %s", bot_test)
logger.debug("Page %s: %s", nextPage, result) if not bot_test["ok"]:
if not result["ok"]: raise ValueError("Bot Token is invalid.")
raise RuntimeError("API Call encountered an error while getting additional integrations: "+unicode(result)) bot_user = slack_bot.api_call("users.info", user=bot_test["user_id"])
iLogs.extend(result["logs"]) logger.debug("User info for Bot token user: %s", bot_user)
integrations = {} if not bot_user["user"]["is_bot"]:
if "integration_whitelist" in config: raise ValueError("Bot Token does not correspond to a bot user.")
integration_whitelist = config["integration_whitelist"]
else:
integration_whitelist = {}
for log in iLogs:
if log["change_type"] in ["added", "enabled", "updated", "expanded", "reissued"]:
status = "active"
elif log["change_type"] == "removed":
status = "removed"
elif log["change_type"] == "disabled":
status = "disabled"
else: else:
status = "unknown" slack_bot = slack_api
logger.warn("Unknown change type (%s) in integration log: %s", log["change_type"], log) integration_issues = check_integrations(slack_api, config)
if "scope" in log and log["scope"]: if integration_issues:
scopes = log["scope"].split(",") logger.info("Found the following integration issues: %s", integration_issues)
notify_problems(integration_issues, config, slack_bot,
"Problem Integrations:", "integration_name", "date")
else: else:
scopes = [] logger.info("No integration issues found.")
if "app_id" in log: user_issues = check_users(slack_api, config)
int_id = log["app_id"] if user_issues:
int_name = log["app_type"] logger.info("Found the following user issues: %s", user_issues)
elif "service_id" in log: notify_problems(user_issues, config, slack_bot, "Problem Users:", "name", "name")
int_id = log["service_id"] else:
int_name = log["service_type"] logger.info("No user issues found.")
elif log["user_id"] == 0 and log["change_type"] == "removed":
# No idea what these are, but they don't have any useful fields, so skip them.
continue def check_integrations(api_client, config):
if "skip_integrations" in config and config["skip_integrations"]:
return {}
result = api_client.api_call("team.integrationLogs")
logger.debug("Integration log results: %s", result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting initial integrations: " +
unicode(result))
iLogs = result["logs"]
if result["paging"]["pages"] > 1:
logger.info("Multiple pages of integration logs exist. Pulling remaining %s pages...",
result["paging"]["pages"] - 1)
while result["paging"]["page"] < result["paging"]["pages"]:
nextPage = result["paging"]["page"] + 1
logger.info("Pulling page %s.", nextPage)
result = api_client.api_call("team.integrationLogs", page=nextPage)
logger.debug("Page %s: %s", nextPage, result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting more integrations: " +
unicode(result))
iLogs.extend(result["logs"])
integrations = {}
if "integration_whitelist" in config:
integration_whitelist = config["integration_whitelist"]
else: else:
logger.warn("Unknown integration type: %s", log) integration_whitelist = {}
continue for log in iLogs:
if int_id not in integrations: if log["change_type"] in ["added", "enabled", "updated", "expanded", "reissued"]:
integrations[int_id] = {"integration_name": int_name, "app_id": int_id, "status": status, "scopes": scopes, "user_id": log["user_id"], "user_name": log["user_name"], "date": log["date"]} status = "active"
if "reason" in log: elif log["change_type"] == "removed":
integrations[int_id]["reason"] = log["reason"] status = "removed"
if "channel" in log: elif log["change_type"] == "disabled":
integrations[int_id]["channel"] = log["channel"] status = "disabled"
problem_integrations = [] else:
if "integration_issue_whitelist" in config: status = "unknown"
global_whitelist = config["integration_issue_whitelist"] logger.warn("Unknown change type (%s) in integration log: %s", log["change_type"], log)
else: if "scope" in log and log["scope"]:
global_whitelist = [] scopes = log["scope"].split(",")
for int_id in integrations: else:
integration = integrations[int_id] scopes = []
if integration["status"] == "removed": if "app_id" in log:
continue int_id = log["app_id"]
if int_id in integration_whitelist: int_name = log["app_type"]
issue_whitelist = global_whitelist.extend(integration_whitelist[int_id]) elif "service_id" in log:
int_id = log["service_id"]
int_name = log["service_type"]
elif log["user_id"] == 0 and log["change_type"] == "removed":
# No idea what these are, but they don't have any useful fields, so skip them.
continue
else:
logger.warn("Unknown integration type: %s", log)
continue
if int_id not in integrations:
integrations[int_id] = {"integration_name": int_name, "app_id": int_id,
"status": status, "scopes": scopes, "user_id": log["user_id"],
"user_name": log["user_name"], "date": log["date"]}
if "reason" in log:
integrations[int_id]["reason"] = log["reason"]
if "channel" in log:
integrations[int_id]["channel"] = log["channel"]
problem_integrations = []
if "integration_issue_whitelist" in config:
global_whitelist = config["integration_issue_whitelist"]
else: else:
issue_whitelist = global_whitelist global_whitelist = []
problems = [] for int_id in integrations:
logger.debug("Checking for issues with integration: %s", integration) integration = integrations[int_id]
if "MAX" in integration["scopes"] and "legacy" not in issue_whitelist: if integration["status"] == "removed":
problems.append("Legacy integration with full access to act as the user") continue
if "admin" in integration["scopes"] and "admin" not in issue_whitelist: if int_id in integration_whitelist:
problems.append("Admin permission") issue_whitelist = global_whitelist.extend(integration_whitelist[int_id])
if "chat:write:user" in integration["scopes"] and "chat:write:user" not in issue_whitelist: else:
problems.append("Can chat as user") issue_whitelist = global_whitelist
if "channels:history" in integration["scopes"] and "channels:history" not in issue_whitelist: problems = []
problems.append("Can access channel history for public channels") logger.debug("Checking for issues with integration: %s", integration)
if "files:read" in integration["scopes"] and "files:read" not in issue_whitelist: if "MAX" in integration["scopes"] and "legacy" not in issue_whitelist:
problems.append("Can read uploaded files") problems.append("Legacy integration with full access to act as the user")
if "files:write:user" in integration["scopes"] and "files:write:user" not in issue_whitelist: if "admin" in integration["scopes"] and "admin" not in issue_whitelist:
problems.append("Can modify/delete existing files") problems.append("Admin permission")
if "groups:history" in integration["scopes"] and "groups:history" not in issue_whitelist: if "chat:write:user" in integration["scopes"] and "chat:write:user" not in issue_whitelist:
problems.append("Can access channel history for private channels") problems.append("Can chat as user")
if "im:history" in integration["scopes"] and "im:history" not in issue_whitelist: if "channels:history" in integration["scopes"] and "channels:history" not in issue_whitelist:
problems.append("Can access channel history for private IMs") problems.append("Can access channel history for public channels")
if "mpim:history" in integration["scopes"] and "mpim:history" not in issue_whitelist: if "files:read" in integration["scopes"] and "files:read" not in issue_whitelist:
problems.append("Can access channel history for multi-party IMs") problems.append("Can read uploaded files")
if "pins:read" in integration["scopes"] and "pins:read" not in issue_whitelist: if "files:write:user" in integration["scopes"] and "files:write:user" not in issue_whitelist:
problems.append("Can access channel pinned messages/files") problems.append("Can modify/delete existing files")
if "search:read" in integration["scopes"] and "search:read" not in issue_whitelist: if "groups:history" in integration["scopes"] and "groups:history" not in issue_whitelist:
problems.append("Can search team files and messages") problems.append("Can access channel history for private channels")
if problems: if "im:history" in integration["scopes"] and "im:history" not in issue_whitelist:
integration["problems"] = problems problems.append("Can access channel history for private IMs")
problem_integrations.append(integration) if "mpim:history" in integration["scopes"] and "mpim:history" not in issue_whitelist:
return problem_integrations problems.append("Can access channel history for multi-party IMs")
if "pins:read" in integration["scopes"] and "pins:read" not in issue_whitelist:
problems.append("Can access channel pinned messages/files")
if "search:read" in integration["scopes"] and "search:read" not in issue_whitelist:
problems.append("Can search team files and messages")
if problems:
integration["problems"] = problems
problem_integrations.append(integration)
return problem_integrations
def check_users(api_client, config): def check_users(api_client, config):
result = api_client.api_call("users.list", presence=False, limit=100) result = api_client.api_call("users.list", presence=False, limit=100)
logger.debug("User list results: %s", result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting initial user list: "+unicode(result))
users = result["members"]
while "next_cursor" in result["response_metadata"] and result["response_metadata"]["next_cursor"]:
logger.info("Further pages of users exist. Pulling next page...")
result = api_client.api_call("users.list", presence=False, limit=100, cursor=result["response_metadata"]["next_cursor"])
logger.debug("User list results: %s", result) logger.debug("User list results: %s", result)
if not result["ok"]: if not result["ok"]:
raise RuntimeError("API Call encountered an error while getting additional user list: "+unicode(result)) raise RuntimeError("API Call encountered an error while getting initial user list: " +
users.extend(result["members"]) unicode(result))
problem_users = [] users = result["members"]
retained_keys = ["real_name", "id", "team_id", "name", "problems", "has_2fa", "two_factor_type", "updated", "is_owner", "is_admin"] while "next_cursor" in result["response_metadata"] and result["response_metadata"]["next_cursor"]:
if "user_whitelist" in config: logger.info("Further pages of users exist. Pulling next page...")
user_whitelist = config["user_whitelist"] result = api_client.api_call("users.list", presence=False, limit=100,
else: cursor=result["response_metadata"]["next_cursor"])
user_whitelist = {} logger.debug("User list results: %s", result)
if "user_issue_whitelist" in config: if not result["ok"]:
global_whitelist = config["user_issue_whitelist"] raise RuntimeError("API Call encountered an error while getting additional users: " +
else: unicode(result))
global_whitelist = [] users.extend(result["members"])
for user in users: problem_users = []
logger.debug("Checking for issues with user: %s", user) retained_keys = ["real_name", "id", "team_id", "name", "problems",
if user["id"] == "USLACKBOT": "has_2fa", "two_factor_type", "updated", "is_owner", "is_admin"]
# Special case Slackbot, since it lacks the fields of other users/bots if "user_whitelist" in config:
continue user_whitelist = config["user_whitelist"]
if user["deleted"] or user["is_bot"]:
continue
if user["id"] in user_whitelist:
issue_whitelist = global_whitelist.extend(user_whitelist[user["id"]])
else: else:
issue_whitelist = global_whitelist user_whitelist = {}
if not user["has_2fa"] and "2fa" not in issue_whitelist: if "user_issue_whitelist" in config:
user["problems"] = ["User does not have 2FA enabled"] global_whitelist = config["user_issue_whitelist"]
if user["has_2fa"] and user["two_factor_type"] == "sms" and "sms" not in issue_whitelist: else:
user["problems"] = ["User is using less-secure SMS-based 2FA"] global_whitelist = []
if "problems" in user: for user in users:
problem_user = {k:v for k,v in user.iteritems() if k in retained_keys} logger.debug("Checking for issues with user: %s", user)
problem_users.append(problem_user) if user["id"] == "USLACKBOT":
return problem_users # Special case Slackbot, since it lacks the fields of other users/bots
continue
if user["deleted"] or user["is_bot"]:
continue
if user["id"] in user_whitelist:
issue_whitelist = global_whitelist.extend(user_whitelist[user["id"]])
else:
issue_whitelist = global_whitelist
if not user["has_2fa"] and "2fa" not in issue_whitelist:
user["problems"] = ["User does not have 2FA enabled"]
if user["has_2fa"] and user["two_factor_type"] == "sms" and "sms" not in issue_whitelist:
user["problems"] = ["User is using less-secure SMS-based 2FA"]
if "problems" in user:
problem_user = {k: v for k, v in user.iteritems() if k in retained_keys}
problem_users.append(problem_user)
return problem_users
def notify_problems(problems, config, slack_bot, heading="Problems:", item_name="name", sort_field=None): def notify_problems(problems, config, slack_bot, heading="Problems:",
indent = "\t" item_name="name", sort_field=None):
if "output_channel" in config: indent = "\t"
indent = "\t\t" if "output_channel" in config:
if sort_field: indent = "\t\t"
problems = sorted(problems, key=lambda k: k[sort_field]) if sort_field:
prob_strings = [] problems = sorted(problems, key=lambda k: k[sort_field])
for problem in problems: prob_strings = []
fmt_problem = problem[item_name]+": "+", ".join(problem["problems"]) for problem in problems:
for item in sorted(problem.items()): fmt_problem = problem[item_name] + ": " + ", ".join(problem["problems"])
if not hasattr(item[1], "strip") and (hasattr(item[1], "__getitem__") or hasattr(item[1], "__iter__")): for item in sorted(problem.items()):
val = ", ".join(item[1]) if not hasattr(item[1], "strip") and \
else: (hasattr(item[1], "__getitem__") or hasattr(item[1], "__iter__")):
val = unicode(item[1]) val = ", ".join(item[1])
fmt_problem= fmt_problem+"\n"+indent+item[0]+": "+val else:
prob_strings.append(fmt_problem) val = unicode(item[1])
formatted_problems = "\n\n".join(prob_strings) fmt_problem = fmt_problem + "\n" + indent + item[0] + ": " + val
if "output_channel" in config: prob_strings.append(fmt_problem)
if len(formatted_problems) < 3000: formatted_problems = "\n\n".join(prob_strings)
attachment = [{ if "output_channel" in config:
"fallback": heading+"\n"+formatted_problems, if len(formatted_problems) < 3000:
"title": heading, attachment = [{
"text": formatted_problems, "fallback": heading + "\n" + formatted_problems,
"color": "#ffe600" "title": heading,
}] "text": formatted_problems,
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"], attachments=attachment, as_user=True) "color": "#ffe600"
}]
result = slack_bot.api_call("chat.postMessage",
channel=config["output_channel"],
attachments=attachment, as_user=True)
else:
# For bigger files we use a snippet. This has a 1MB limit.
# If you have more problems... well add this to the list.
result = slack_bot.api_call("files.upload",
content=formatted_problems,
title=heading, filetype="text")
logger.info("Message too large. Uploaded as file: %s", result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while uploading file: " +
unicode(result))
msg = "*" + heading + "*\n\nToo many " + "issues to post directly.\nSee <" + \
result["file"]["url_private"] + "|the uploaded file> for details."
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"],
text=msg, unfurl_links=True, as_user=True)
logger.info("Message posted. Result: %s", result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while posting message: " +
unicode(result))
else: else:
# For bigger files we use a snippet. This has a 1MB limit. If you have more problems... well add that to the list. print heading
result = slack_bot.api_call("files.upload", content=formatted_problems, title=heading, filetype="text") print ""
logger.info("Message too large. Uploaded as file: %s", result) print formatted_problems
if not result["ok"]:
raise RuntimeError("API Call encountered an error while uploading file: "+unicode(result))
result = slack_bot.api_call("chat.postMessage", channel=config["output_channel"], text="*"+heading+"*\n\nToo many issues to post directly.\nSee <"+result["file"]["url_private"]+"|the uploaded file> for more information.", unfurl_links=True, as_user=True)
logger.info("Message posted. Result: %s", result)
if not result["ok"]:
raise RuntimeError("API Call encountered an error while posting message: "+unicode(result))
else:
print heading
print ""
print formatted_problems
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog="Belay", description="Check the security of your Slack.") parser = argparse.ArgumentParser(prog="Belay",
parser.add_argument("-c", "--config", default=None, help="Non-standard location of a configuration file.") description="Secure your Slack.")
parser.add_argument("-f", "--file", default=None, help="File to redirect log output into.") parser.add_argument("-c", "--config", default=None,
parser.add_argument("-t", "--team", default=None, help="Team to check for multi-team configs.") help="Non-standard location of a configuration file.")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase log verbosity level. (Default level: WARN, use twice for DEBUG)") parser.add_argument("-f", "--file", default=None,
parser.add_argument("-V", "--version", action="version", version="%(prog)s "+program_version, help="Display version information and exit.") help="File to redirect log output into.")
args = parser.parse_args() parser.add_argument("-t", "--team", default=None,
loglevel = max(10, 30 - (args.verbose * 10)) help="Team to check for multi-team configs.")
logformat = '%(asctime)s %(levelname)s: %(message)s' parser.add_argument("-v", "--verbose", action="count", default=0,
if args.file: help="Increase log verbosity level. (Default" +
logging.basicConfig(filename=args.file, level=loglevel, format=logformat) " level: WARN, use twice for DEBUG)")
else: parser.add_argument("-V", "--version", action="version",
logging.basicConfig(level=loglevel, format=logformat) version="%(prog)s " + program_version,
help="Display version information and exit.")
args = parser.parse_args()
loglevel = max(10, 30 - (args.verbose * 10))
logformat = '%(asctime)s %(levelname)s: %(message)s'
if args.file:
logging.basicConfig(filename=args.file, level=loglevel, format=logformat)
else:
logging.basicConfig(level=loglevel, format=logformat)
logger.info("Belaying...") logger.info("Belaying...")
# try: try:
config = load_config(args.config, args.team) config = load_config(args.config, args.team)
belay(config) belay(config)
# except Exception as e: except Exception as e:
# sys.exit(e) sys.exit(e)
# finally: finally:
logging.shutdown() logging.shutdown()

Loading…
Cancel
Save