refactored most binaries
added AnsibleOptionsError removed pulicate parser error class
This commit is contained in:
parent
900b992ba9
commit
cdefeb6d84
8 changed files with 754 additions and 687 deletions
|
@ -40,13 +40,15 @@ def get_config(p, section, key, env_var, default, boolean=False, integer=False,
|
|||
''' return a configuration variable with casting '''
|
||||
value = _get_config(p, section, key, env_var, default)
|
||||
if boolean:
|
||||
return mk_boolean(value)
|
||||
if value and integer:
|
||||
return int(value)
|
||||
if value and floating:
|
||||
return float(value)
|
||||
if value and islist:
|
||||
return [x.strip() for x in value.split(',')]
|
||||
value = mk_boolean(value)
|
||||
if value:
|
||||
if integer:
|
||||
value = int(value)
|
||||
if floating:
|
||||
value = float(value)
|
||||
if islist:
|
||||
if isinstance(value, basestring):
|
||||
value = [x.strip() for x in value.split(',')]
|
||||
return value
|
||||
|
||||
def _get_config(p, section, key, env_var, default):
|
||||
|
@ -104,7 +106,7 @@ DEFAULTS='defaults'
|
|||
|
||||
# configurable things
|
||||
DEFAULT_DEBUG = get_config(p, DEFAULTS, 'debug', 'ANSIBLE_DEBUG', False, boolean=True)
|
||||
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'inventory', 'ANSIBLE_INVENTORY', get_config(p, DEFAULTS,'hostfile','ANSIBLE_HOSTS', '/etc/ansible/hosts')))
|
||||
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'hostfile', 'ANSIBLE_HOSTS', get_config(p, DEFAULTS,'inventory','ANSIBLE_INVENTORY', '/etc/ansible/hosts')))
|
||||
DEFAULT_MODULE_PATH = get_config(p, DEFAULTS, 'library', 'ANSIBLE_LIBRARY', None)
|
||||
DEFAULT_ROLES_PATH = shell_expand_path(get_config(p, DEFAULTS, 'roles_path', 'ANSIBLE_ROLES_PATH', '/etc/ansible/roles'))
|
||||
DEFAULT_REMOTE_TMP = get_config(p, DEFAULTS, 'remote_tmp', 'ANSIBLE_REMOTE_TEMP', '$HOME/.ansible/tmp')
|
||||
|
@ -212,6 +214,7 @@ GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY
|
|||
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
|
||||
|
||||
# non-configurable things
|
||||
MODULE_REQUIRE_ARGS = ['command', 'shell', 'raw', 'script']
|
||||
DEFAULT_BECOME_PASS = None
|
||||
DEFAULT_SUDO_PASS = None
|
||||
DEFAULT_REMOTE_PASS = None
|
||||
|
|
|
@ -140,6 +140,10 @@ class AnsibleError(Exception):
|
|||
|
||||
return error_message
|
||||
|
||||
class AnsibleOptionsError(AnsibleError):
|
||||
''' bad or incomplete options passed '''
|
||||
pass
|
||||
|
||||
class AnsibleParserError(AnsibleError):
|
||||
''' something was detected early that is wrong about a playbook or data file '''
|
||||
pass
|
||||
|
@ -164,6 +168,14 @@ class AnsibleFilterError(AnsibleRuntimeError):
|
|||
''' a templating failure '''
|
||||
pass
|
||||
|
||||
class AnsibleLookupError(AnsibleRuntimeError):
|
||||
''' a lookup failure '''
|
||||
pass
|
||||
|
||||
class AnsibleCallbackError(AnsibleRuntimeError):
|
||||
''' a callback failure '''
|
||||
pass
|
||||
|
||||
class AnsibleUndefinedVariable(AnsibleRuntimeError):
|
||||
''' a templating failure '''
|
||||
pass
|
||||
|
@ -171,7 +183,3 @@ class AnsibleUndefinedVariable(AnsibleRuntimeError):
|
|||
class AnsibleFileNotFound(AnsibleRuntimeError):
|
||||
''' a file missing failure '''
|
||||
pass
|
||||
|
||||
class AnsibleParserError(AnsibleRuntimeError):
|
||||
''' a parser error '''
|
||||
pass
|
||||
|
|
|
@ -36,6 +36,8 @@ class GalaxyRole(object):
|
|||
SUPPORTED_SCMS = set(['git', 'hg'])
|
||||
META_MAIN = os.path.join('meta', 'main.yml')
|
||||
META_INSTALL = os.path.join('meta', '.galaxy_install_info')
|
||||
ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars')
|
||||
|
||||
|
||||
def __init__(self, galaxy, role_name, role_version=None, role_url=None):
|
||||
|
||||
|
@ -45,13 +47,13 @@ class GalaxyRole(object):
|
|||
self.name = role_name
|
||||
self.meta_data = None
|
||||
self.install_info = None
|
||||
self.role_path = (os.path.join(self.roles_path, self.name))
|
||||
self.path = (os.path.join(galaxy.roles_path, self.name))
|
||||
|
||||
# TODO: possibly parse version and url from role_name
|
||||
self.version = role_version
|
||||
self.url = role_url
|
||||
if self.url is None and '://' in self.name:
|
||||
self.url = self.name
|
||||
if self.url is None:
|
||||
self._spec_parse()
|
||||
|
||||
if C.GALAXY_SCMS:
|
||||
self.scms = self.SUPPORTED_SCMS.intersection(set(C.GALAXY_SCMS))
|
||||
|
@ -62,7 +64,7 @@ class GalaxyRole(object):
|
|||
self.display.warning("No valid SCMs configured for Galaxy.")
|
||||
|
||||
|
||||
def fetch_from_scm_archive(self, scm, role_url, role_version):
|
||||
def fetch_from_scm_archive(self):
|
||||
|
||||
# this can be configured to prevent unwanted SCMS but cannot add new ones unless the code is also updated
|
||||
if scm not in self.scms:
|
||||
|
@ -111,12 +113,21 @@ class GalaxyRole(object):
|
|||
return temp_file.name
|
||||
|
||||
|
||||
def get_metadata(self):
|
||||
"""
|
||||
Returns role metadata
|
||||
"""
|
||||
if self.meta_data is None:
|
||||
self._read_metadata
|
||||
|
||||
def read_metadata(self):
|
||||
return self.meta_data
|
||||
|
||||
|
||||
def _read_metadata(self):
|
||||
"""
|
||||
Reads the metadata as YAML, if the file 'meta/main.yml' exists
|
||||
"""
|
||||
meta_path = os.path.join(self.role_path, self.META_MAIN)
|
||||
meta_path = os.path.join(self.path, self.META_MAIN)
|
||||
if os.path.isfile(meta_path):
|
||||
try:
|
||||
f = open(meta_path, 'r')
|
||||
|
@ -127,15 +138,24 @@ class GalaxyRole(object):
|
|||
finally:
|
||||
f.close()
|
||||
|
||||
return True
|
||||
|
||||
def read_galaxy_install_info(self):
|
||||
def get_galaxy_install_info(self):
|
||||
"""
|
||||
Returns role install info
|
||||
"""
|
||||
if self.install_info is None:
|
||||
self._read_galaxy_isntall_info()
|
||||
|
||||
return self.install_info
|
||||
|
||||
|
||||
def _read_galaxy_install_info(self):
|
||||
"""
|
||||
Returns the YAML data contained in 'meta/.galaxy_install_info',
|
||||
if it exists.
|
||||
"""
|
||||
|
||||
info_path = os.path.join(self.role_path, self.META_INSTALL)
|
||||
info_path = os.path.join(self.path, self.META_INSTALL)
|
||||
if os.path.isfile(info_path):
|
||||
try:
|
||||
f = open(info_path, 'r')
|
||||
|
@ -146,9 +166,7 @@ class GalaxyRole(object):
|
|||
finally:
|
||||
f.close()
|
||||
|
||||
return True
|
||||
|
||||
def write_galaxy_install_info(self):
|
||||
def _write_galaxy_install_info(self):
|
||||
"""
|
||||
Writes a YAML-formatted file to the role's meta/ directory
|
||||
(named .galaxy_install_info) which contains some information
|
||||
|
@ -159,7 +177,7 @@ class GalaxyRole(object):
|
|||
version=self.version,
|
||||
install_date=datetime.datetime.utcnow().strftime("%c"),
|
||||
)
|
||||
info_path = os.path.join(self.role_path, self.META_INSTALL)
|
||||
info_path = os.path.join(self.path, self.META_INSTALL)
|
||||
try:
|
||||
f = open(info_path, 'w+')
|
||||
self.install_info = yaml.safe_dump(info, f)
|
||||
|
@ -178,7 +196,7 @@ class GalaxyRole(object):
|
|||
"""
|
||||
if self.read_metadata():
|
||||
try:
|
||||
rmtree(self.role_path)
|
||||
rmtree(self.path)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
@ -213,7 +231,7 @@ class GalaxyRole(object):
|
|||
self.display.error("failed to download the file.")
|
||||
return False
|
||||
|
||||
def install(self, role_version, role_filename):
|
||||
def install(self, role_filename):
|
||||
# the file is a tar, so open it that way and extract it
|
||||
# to the specified (or default) roles directory
|
||||
|
||||
|
@ -246,10 +264,10 @@ class GalaxyRole(object):
|
|||
# we strip off the top-level directory for all of the files contained within
|
||||
# the tar file here, since the default is 'github_repo-target', and change it
|
||||
# to the specified role's name
|
||||
self.display.display("- extracting %s to %s" % (self.name, self.role_path))
|
||||
self.display.display("- extracting %s to %s" % (self.name, self.path))
|
||||
try:
|
||||
if os.path.exists(self.role_path):
|
||||
if not os.path.isdir(self.role_path):
|
||||
if os.path.exists(self.path):
|
||||
if not os.path.isdir(self.path):
|
||||
self.display.error("the specified roles path exists and is not a directory.")
|
||||
return False
|
||||
elif not getattr(self.options, "force", False):
|
||||
|
@ -258,13 +276,13 @@ class GalaxyRole(object):
|
|||
else:
|
||||
# using --force, remove the old path
|
||||
if not self.remove():
|
||||
self.display.error("%s doesn't appear to contain a role." % self.role_path)
|
||||
self.display.error("%s doesn't appear to contain a role." % self.path)
|
||||
self.display.error(" please remove this directory manually if you really want to put the role here.")
|
||||
return False
|
||||
else:
|
||||
os.makedirs(self.role_path)
|
||||
os.makedirs(self.path)
|
||||
|
||||
# now we do the actual extraction to the role_path
|
||||
# now we do the actual extraction to the path
|
||||
for member in members:
|
||||
# we only extract files, and remove any relative path
|
||||
# bits that might be in the file for security purposes
|
||||
|
@ -276,15 +294,62 @@ class GalaxyRole(object):
|
|||
if part != '..' and '~' not in part and '$' not in part:
|
||||
final_parts.append(part)
|
||||
member.name = os.path.join(*final_parts)
|
||||
role_tar_file.extract(member, self.role_path)
|
||||
role_tar_file.extract(member, self.path)
|
||||
|
||||
# write out the install info file for later use
|
||||
self.version = role_version
|
||||
self.write_galaxy_install_info()
|
||||
self._write_galaxy_install_info()
|
||||
except OSError as e:
|
||||
self.display.error("Could not update files in %s: %s" % (self.role_path, str(e)))
|
||||
self.display.error("Could not update files in %s: %s" % (self.path, str(e)))
|
||||
return False
|
||||
|
||||
# return the parsed yaml metadata
|
||||
self.display.display("- %s was installed successfully" % self.role_name)
|
||||
self.display.display("- %s was installed successfully" % self.name)
|
||||
return True
|
||||
|
||||
def get_spec(self):
|
||||
"""
|
||||
Returns role spec info
|
||||
{
|
||||
'scm': 'git',
|
||||
'src': 'http://git.example.com/repos/repo.git',
|
||||
'version': 'v1.0',
|
||||
'name': 'repo'
|
||||
}
|
||||
"""
|
||||
if self.scm is None and self.url is None:
|
||||
self._read_galaxy_isntall_info()
|
||||
|
||||
return dict(scm=self.scm, src=self.url, version=self.version, role_name=self.name)
|
||||
|
||||
def _spec_parse(self):
|
||||
''' creates separated parts of role spec '''
|
||||
default_role_versions = dict(git='master', hg='tip')
|
||||
|
||||
if not self.url and '://' in self.name:
|
||||
role_spec = self.name.strip()
|
||||
|
||||
if role_spec == "" or role_spec.startswith("#"):
|
||||
return
|
||||
|
||||
tokens = [s.strip() for s in role_spec.split(',')]
|
||||
|
||||
# assume https://github.com URLs are git+https:// URLs and not tarballs unless they end in '.zip'
|
||||
if 'github.com/' in tokens[0] and not tokens[0].startswith("git+") and not tokens[0].endswith('.tar.gz'):
|
||||
tokens[0] = 'git+' + tokens[0]
|
||||
|
||||
if '+' in tokens[0]:
|
||||
(self.scm, self.url) = tokens[0].split('+')
|
||||
else:
|
||||
self.scm = None
|
||||
self.url = tokens[0]
|
||||
|
||||
if len(tokens) >= 2:
|
||||
self.version = tokens[1]
|
||||
|
||||
if len(tokens) == 3:
|
||||
self.name = tokens[2]
|
||||
else:
|
||||
self.name = self._repo_url_to_role_name(tokens[0])
|
||||
|
||||
if self.scm and not self.version:
|
||||
self.version = default_role_versions.get(scm, '')
|
||||
|
|
|
@ -28,6 +28,7 @@ import getpass
|
|||
|
||||
from ansible import __version__
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
# FIXME: documentation for methods here, which have mostly been
|
||||
|
@ -40,11 +41,154 @@ class SortedOptParser(optparse.OptionParser):
|
|||
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
|
||||
return optparse.OptionParser.format_help(self, formatter=None)
|
||||
|
||||
#TODO: move many cli only functions in this file into the CLI class
|
||||
class CLI(object):
|
||||
''' code behind bin/ansible* programs '''
|
||||
|
||||
VALID_ACTIONS = ['No Actions']
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
"""
|
||||
Base init method for all command line programs
|
||||
"""
|
||||
|
||||
self.args = args
|
||||
self.options = None
|
||||
self.parser = None
|
||||
self.action = None
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
|
||||
def set_action(self):
|
||||
"""
|
||||
Get the action the user wants to execute from the sys argv list.
|
||||
"""
|
||||
for i in range(0,len(self.args)):
|
||||
arg = self.args[i]
|
||||
if arg in self.VALID_ACTIONS:
|
||||
self.action = arg
|
||||
del self.args[i]
|
||||
break
|
||||
|
||||
if not self.action:
|
||||
self.parser.print_help()
|
||||
raise AnsibleError("Missing required action")
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
Actually runs a child defined method using the execute_<action> pattern
|
||||
"""
|
||||
fn = getattr(self, "execute_%s" % self.action)
|
||||
fn()
|
||||
|
||||
def parse(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
def run(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
@staticmethod
|
||||
def ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=False, confirm_vault=False, confirm_new=False):
|
||||
|
||||
vault_pass = None
|
||||
new_vault_pass = None
|
||||
|
||||
if ask_vault_pass:
|
||||
vault_pass = getpass.getpass(prompt="Vault password: ")
|
||||
|
||||
if ask_vault_pass and confirm_vault:
|
||||
vault_pass2 = getpass.getpass(prompt="Confirm Vault password: ")
|
||||
if vault_pass != vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
if ask_new_vault_pass:
|
||||
new_vault_pass = getpass.getpass(prompt="New Vault password: ")
|
||||
|
||||
if ask_new_vault_pass and confirm_new:
|
||||
new_vault_pass2 = getpass.getpass(prompt="Confirm New Vault password: ")
|
||||
if new_vault_pass != new_vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
# enforce no newline chars at the end of passwords
|
||||
if vault_pass:
|
||||
vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
if new_vault_pass:
|
||||
new_vault_pass = to_bytes(new_vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
|
||||
return vault_pass, new_vault_pass
|
||||
|
||||
|
||||
def ask_passwords(self):
|
||||
|
||||
op = self.options
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
become_prompt = ''
|
||||
|
||||
if op.ask_pass:
|
||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||
become_prompt = "%s password[defaults to SSH password]: " % op.become_method.upper()
|
||||
if sshpass:
|
||||
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
|
||||
else:
|
||||
become_prompt = "%s password: " % op.become_method.upper()
|
||||
|
||||
if op.become_ask_pass:
|
||||
becomepass = getpass.getpass(prompt=become_prompt)
|
||||
if op.ask_pass and becomepass == '':
|
||||
becomepass = sshpass
|
||||
if becomepass:
|
||||
becomepass = to_bytes(becomepass)
|
||||
|
||||
return (sshpass, becomepass)
|
||||
|
||||
|
||||
def normalize_become_options(self):
|
||||
''' this keeps backwards compatibility with sudo/su self.options '''
|
||||
self.options.become_ask_pass = self.options.become_ask_pass or self.options.ask_sudo_pass or self.options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
|
||||
self.options.become_user = self.options.become_user or self.options.sudo_user or self.options.su_user or C.DEFAULT_BECOME_USER
|
||||
|
||||
if self.options.become:
|
||||
pass
|
||||
elif self.options.sudo:
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
elif self.options.su:
|
||||
self.options.become = True
|
||||
options.become_method = 'su'
|
||||
|
||||
|
||||
def validate_conflicts(self):
|
||||
|
||||
op = self.options
|
||||
|
||||
# Check for vault related conflicts
|
||||
if (op.ask_vault_pass and op.vault_password_file):
|
||||
self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
|
||||
|
||||
|
||||
# Check for privilege escalation conflicts
|
||||
if (op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) or \
|
||||
(op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass) or \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass):
|
||||
|
||||
self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||
"and su arguments ('-su', '--su-user', and '--ask-su-pass') "
|
||||
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
|
||||
" are exclusive of each other")
|
||||
|
||||
@staticmethod
|
||||
def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False,
|
||||
async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False):
|
||||
''' create an options parser for any ansible script '''
|
||||
|
||||
parser = SortedOptParser(usage, version=version("%prog"))
|
||||
parser = SortedOptParser(usage, version=CLI.version("%prog"))
|
||||
|
||||
parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
|
||||
help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
|
||||
|
@ -144,6 +288,7 @@ def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False,
|
|||
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def version(prog):
|
||||
result = "{0} {1}".format(prog, __version__)
|
||||
gitinfo = _gitinfo()
|
||||
|
@ -152,6 +297,7 @@ def version(prog):
|
|||
result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def version_info(gitinfo=False):
|
||||
if gitinfo:
|
||||
# expensive call, user with care
|
||||
|
@ -234,69 +380,3 @@ def _gitinfo():
|
|||
result += "\n {0}: {1}".format(submodule_path, submodule_info)
|
||||
f.close()
|
||||
return result
|
||||
|
||||
|
||||
def ask_passwords(options):
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vaultpass = None
|
||||
become_prompt = ''
|
||||
|
||||
if options.ask_pass:
|
||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||
become_prompt = "%s password[defaults to SSH password]: " % options.become_method.upper()
|
||||
if sshpass:
|
||||
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
|
||||
else:
|
||||
become_prompt = "%s password: " % options.become_method.upper()
|
||||
|
||||
if options.become_ask_pass:
|
||||
becomepass = getpass.getpass(prompt=become_prompt)
|
||||
if options.ask_pass and becomepass == '':
|
||||
becomepass = sshpass
|
||||
if becomepass:
|
||||
becomepass = to_bytes(becomepass)
|
||||
|
||||
if options.ask_vault_pass:
|
||||
vaultpass = getpass.getpass(prompt="Vault password: ")
|
||||
if vaultpass:
|
||||
vaultpass = to_bytes(vaultpass, errors='strict', nonstring='simplerepr').strip()
|
||||
|
||||
return (sshpass, becomepass, vaultpass)
|
||||
|
||||
|
||||
def normalize_become_options(options):
|
||||
''' this keeps backwards compatibility with sudo/su options '''
|
||||
options.become_ask_pass = options.become_ask_pass or options.ask_sudo_pass or options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
|
||||
options.become_user = options.become_user or options.sudo_user or options.su_user or C.DEFAULT_BECOME_USER
|
||||
|
||||
if options.become:
|
||||
pass
|
||||
elif options.sudo:
|
||||
options.become = True
|
||||
options.become_method = 'sudo'
|
||||
elif options.su:
|
||||
options.become = True
|
||||
options.become_method = 'su'
|
||||
|
||||
|
||||
def validate_conflicts(parser, options):
|
||||
|
||||
# Check for vault related conflicts
|
||||
if (options.ask_vault_pass and options.vault_password_file):
|
||||
parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
|
||||
|
||||
|
||||
# Check for privilege escalation conflicts
|
||||
if (options.su or options.su_user or options.ask_su_pass) and \
|
||||
(options.sudo or options.sudo_user or options.ask_sudo_pass) or \
|
||||
(options.su or options.su_user or options.ask_su_pass) and \
|
||||
(options.become or options.become_user or options.become_ask_pass) or \
|
||||
(options.sudo or options.sudo_user or options.ask_sudo_pass) and \
|
||||
(options.become or options.become_user or options.become_ask_pass):
|
||||
|
||||
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||
"and su arguments ('-su', '--su-user', and '--ask-su-pass') "
|
||||
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
|
||||
" are exclusive of each other")
|
||||
|
||||
|
|
112
v2/bin/ansible
112
v2/bin/ansible
|
@ -40,28 +40,20 @@ from ansible.inventory import Inventory
|
|||
from ansible.parsing import DataLoader
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.playbook.play import Play
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.cli import base_parser, validate_conflicts, normalize_become_options, ask_passwords
|
||||
from ansible.utils.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.vault import read_vault_file
|
||||
from ansible.vars import VariableManager
|
||||
|
||||
########################################################
|
||||
|
||||
class Cli(object):
|
||||
''' code behind bin/ansible '''
|
||||
|
||||
def __init__(self, display=None):
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
class AdHocCli(CLI):
|
||||
''' code behind ansible ad-hoc cli'''
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
parser = base_parser(
|
||||
self.parser = CLI.base_parser(
|
||||
usage='%prog <host-pattern> [options]',
|
||||
runas_opts=True,
|
||||
async_opts=True,
|
||||
|
@ -71,102 +63,110 @@ class Cli(object):
|
|||
)
|
||||
|
||||
# options unique to ansible ad-hoc
|
||||
parser.add_option('-a', '--args', dest='module_args',
|
||||
self.parser.add_option('-a', '--args', dest='module_args',
|
||||
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
|
||||
parser.add_option('-m', '--module-name', dest='module_name',
|
||||
self.parser.add_option('-m', '--module-name', dest='module_name',
|
||||
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
|
||||
default=C.DEFAULT_MODULE_NAME)
|
||||
|
||||
options, args = parser.parse_args()
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
|
||||
if len(args) == 0 or len(args) > 1:
|
||||
parser.print_help()
|
||||
if len(self.args) != 1:
|
||||
self.parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
display.verbosity = options.verbosity
|
||||
validate_conflicts(parser,options)
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
return (options, args)
|
||||
return True
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
def run(self, options, args):
|
||||
def run(self):
|
||||
''' use Runner lib to do SSH things '''
|
||||
|
||||
pattern = args[0]
|
||||
# only thing left should be host pattern
|
||||
pattern = self.args[0]
|
||||
|
||||
if options.connection == "local":
|
||||
options.ask_pass = False
|
||||
# ignore connection password cause we are local
|
||||
if self.options.connection == "local":
|
||||
self.options.ask_pass = False
|
||||
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
|
||||
normalize_become_options(options)
|
||||
(sshpass, becomepass, vault_pass) = ask_passwords(options)
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if options.vault_password_file:
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(options.vault_password_file)
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
variable_manager = VariableManager()
|
||||
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=options.inventory)
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
|
||||
hosts = inventory.list_hosts(pattern)
|
||||
if len(hosts) == 0:
|
||||
d = Display()
|
||||
d.warning("provided hosts list is empty, only localhost is available")
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
|
||||
if options.listhosts:
|
||||
if self.options.listhosts:
|
||||
for host in hosts:
|
||||
self.display.display(' %s' % host.name)
|
||||
sys.exit(0)
|
||||
return 0
|
||||
|
||||
if ((options.module_name == 'command' or options.module_name == 'shell') and not options.module_args):
|
||||
raise AnsibleError("No argument passed to %s module" % options.module_name)
|
||||
if self.options.module_name in C.MODULE_REQUIRE_ARGS and not self.options.module_args:
|
||||
raise AnsibleError("No argument passed to %s module" % self.options.module_name)
|
||||
|
||||
# FIXME: async support needed
|
||||
#if options.seconds:
|
||||
#TODO: implement async support
|
||||
#if self.options.seconds:
|
||||
# callbacks.display("background launch...\n\n", color='cyan')
|
||||
# results, poller = runner.run_async(options.seconds)
|
||||
# results = self.poll_while_needed(poller, options)
|
||||
# results, poller = runner.run_async(self.options.seconds)
|
||||
# results = self.poll_while_needed(poller)
|
||||
#else:
|
||||
# results = runner.run()
|
||||
|
||||
# create a pseudo-play to execute the specified module via a single task
|
||||
play_ds = dict(
|
||||
name = "Ansible Ad-Hoc",
|
||||
hosts = pattern,
|
||||
gather_facts = 'no',
|
||||
tasks = [
|
||||
dict(action=dict(module=options.module_name, args=parse_kv(options.module_args))),
|
||||
]
|
||||
tasks = [ dict(action=dict(module=self.options.module_name, args=parse_kv(self.options.module_args))), ]
|
||||
)
|
||||
|
||||
play = Play().load(play_ds, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
# now create a task queue manager to execute the play
|
||||
try:
|
||||
display = Display()
|
||||
tqm = TaskQueueManager(inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=passwords, stdout_callback='minimal')
|
||||
tqm = TaskQueueManager(
|
||||
inventory=inventory,
|
||||
callback='minimal',
|
||||
variable_manager=variable_manager,
|
||||
loader=loader,
|
||||
display=self.display,
|
||||
options=self.options,
|
||||
passwords=passwords,
|
||||
stdout_callback='minimal',
|
||||
)
|
||||
result = tqm.run(play)
|
||||
finally:
|
||||
if tqm:
|
||||
tqm.cleanup()
|
||||
except AnsibleError:
|
||||
tqm.cleanup()
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
def poll_while_needed(self, poller, options):
|
||||
def poll_while_needed(self, poller):
|
||||
''' summarize results from Runner '''
|
||||
|
||||
# BACKGROUND POLL LOGIC when -B and -P are specified
|
||||
if options.seconds and options.poll_interval > 0:
|
||||
poller.wait(options.seconds, options.poll_interval)
|
||||
if self.options.seconds and self.options.poll_interval > 0:
|
||||
poller.wait(self.options.seconds, self.options.poll_interval)
|
||||
|
||||
return poller.results
|
||||
|
||||
|
@ -176,14 +176,12 @@ class Cli(object):
|
|||
if __name__ == '__main__':
|
||||
|
||||
display = Display()
|
||||
#display.display(" ".join(sys.argv))
|
||||
|
||||
try:
|
||||
cli = Cli(display=display)
|
||||
(options, args) = cli.parse()
|
||||
sys.exit(cli.run(options, args))
|
||||
cli = AdHocCli(sys.argv, display=display)
|
||||
cli.parse()
|
||||
sys.exit(cli.run())
|
||||
except AnsibleError as e:
|
||||
display.error(str(e))
|
||||
display.display(str(e), stderr=True, color='red')
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
display.error("interrupted")
|
||||
|
|
|
@ -42,113 +42,109 @@ from optparse import OptionParser
|
|||
import ansible.constants as C
|
||||
import ansible.utils
|
||||
import ansible.galaxy
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.galaxy import Galaxy
|
||||
from ansible.galaxy.api import GalaxyAPI
|
||||
from ansible.galaxy.role import GalaxyRole
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.cli import CLI
|
||||
|
||||
class Cli(object):
|
||||
class GalaxyCLI(CLI):
|
||||
|
||||
VALID_ACTIONS = ("init", "info", "install", "list", "remove")
|
||||
SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
|
||||
|
||||
def __init__(self):
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
self.action = None
|
||||
|
||||
def set_action(args):
|
||||
"""
|
||||
Get the action the user wants to execute from the
|
||||
sys argv list.
|
||||
"""
|
||||
for i in range(0,len(args)):
|
||||
arg = args[i]
|
||||
if arg in VALID_ACTIONS:
|
||||
del args[i]
|
||||
self.action = arg
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.api = None
|
||||
self.galaxy = None
|
||||
super(GalaxyCLI, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(VALID_ACTIONS)
|
||||
|
||||
usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS)
|
||||
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
|
||||
OptionParser.format_epilog = lambda self, formatter: self.epilog
|
||||
parser = OptionParser(usage=usage, epilog=epilog)
|
||||
|
||||
if not self.action:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
self.parser = parser
|
||||
self.set_action()
|
||||
|
||||
# options specific to actions
|
||||
if self.action == "info":
|
||||
parser.set_usage("usage: %prog info [options] role_name[,version]")
|
||||
self.parser.set_usage("usage: %prog info [options] role_name[,version]")
|
||||
elif self.action == "init":
|
||||
parser.set_usage("usage: %prog init [options] role_name")
|
||||
parser.add_option(
|
||||
self.parser.set_usage("usage: %prog init [options] role_name")
|
||||
self.parser.add_option(
|
||||
'-p', '--init-path', dest='init_path', default="./",
|
||||
help='The path in which the skeleton role will be created. '
|
||||
'The default is the current working directory.')
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'--offline', dest='offline', default=False, action='store_true',
|
||||
help="Don't query the galaxy API when creating roles")
|
||||
elif self.action == "install":
|
||||
parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
|
||||
parser.add_option(
|
||||
self.parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
|
||||
self.parser.add_option(
|
||||
'-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
||||
help='Ignore errors and continue with the next specified role.')
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'-n', '--no-deps', dest='no_deps', action='store_true', default=False,
|
||||
help='Don\'t download roles listed as dependencies')
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'-r', '--role-file', dest='role_file',
|
||||
help='A file containing a list of roles to be imported')
|
||||
elif self.action == "remove":
|
||||
parser.set_usage("usage: %prog remove role1 role2 ...")
|
||||
self.parser.set_usage("usage: %prog remove role1 role2 ...")
|
||||
elif self.action == "list":
|
||||
parser.set_usage("usage: %prog list [role_name]")
|
||||
self.parser.set_usage("usage: %prog list [role_name]")
|
||||
|
||||
# options that apply to more than one action
|
||||
if self.action != "init":
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH,
|
||||
help='The path to the directory containing your roles. '
|
||||
'The default is the roles_path configured in your '
|
||||
'ansible.cfg file (/etc/ansible/roles if not configured)')
|
||||
|
||||
if self.action in ("info","init","install"):
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'-s', '--server', dest='api_server', default="galaxy.ansible.com",
|
||||
help='The API server destination')
|
||||
|
||||
if self.action in ("init","install"):
|
||||
parser.add_option(
|
||||
self.parser.add_option(
|
||||
'-f', '--force', dest='force', action='store_true', default=False,
|
||||
help='Force overwriting an existing role')
|
||||
|
||||
# done, return the parser
|
||||
options, args = parser.parse_args()
|
||||
# get options, args and galaxy object
|
||||
self.options, self.args =self.parser.parse_args()
|
||||
self.galaxy = Galaxy(self.options, self.display)
|
||||
|
||||
if len(args) == 0 or len(args) > 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
if len(self.args) != 1:
|
||||
raise AnsibleOptionsError("Missing arguments")
|
||||
|
||||
display.verbosity = options.verbosity
|
||||
return True
|
||||
|
||||
return (options, args)
|
||||
def run(self):
|
||||
|
||||
def run(options, args):
|
||||
#self.display.verbosity = self.options.verbosity
|
||||
api_server = self.get_opt("api_server", "galaxy.ansible.com")
|
||||
|
||||
# execute the desired action
|
||||
fn = getattr(self, "execute_%s" % self.action)
|
||||
fn(args, options)
|
||||
# if not offline, get connect to galaxy api
|
||||
if self.action == 'init' and not self.options.offline:
|
||||
self.api = GalaxyAPI(self.galaxy, api_server)
|
||||
if not self.api:
|
||||
raise AnsibleError("The API server (%s) is not responding, please try again later." % api_server)
|
||||
|
||||
def get_opt(options, k, defval=""):
|
||||
self.execute()
|
||||
|
||||
def get_opt(self, k, defval=""):
|
||||
"""
|
||||
Returns an option from an Optparse values instance.
|
||||
"""
|
||||
try:
|
||||
data = getattr(options, k)
|
||||
data = getattr(self.options, k)
|
||||
except:
|
||||
return defval
|
||||
if k == "roles_path":
|
||||
|
@ -156,56 +152,40 @@ class Cli(object):
|
|||
data = data.split(os.pathsep)[0]
|
||||
return data
|
||||
|
||||
def exit_without_ignore(options, rc=1):
|
||||
def exit_without_ignore(self, rc=1):
|
||||
"""
|
||||
Exits with the specified return code unless the
|
||||
option --ignore-errors was specified
|
||||
"""
|
||||
|
||||
if not get_opt(options, "ignore_errors", False):
|
||||
print '- you can use --ignore-errors to skip failed roles.'
|
||||
sys.exit(rc)
|
||||
if not self.get_opt("ignore_errors", False):
|
||||
self.display.error('- you can use --ignore-errors to skip failed tasks/roles.')
|
||||
return rc
|
||||
|
||||
|
||||
|
||||
def execute_init(args, options, parser):
|
||||
def execute_init(self):
|
||||
"""
|
||||
Executes the init action, which creates the skeleton framework
|
||||
of a role that complies with the galaxy metadata format.
|
||||
"""
|
||||
|
||||
init_path = get_opt(options, 'init_path', './')
|
||||
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
||||
force = get_opt(options, 'force', False)
|
||||
offline = get_opt(options, 'offline', False)
|
||||
init_path = self.get_opt('init_path', './')
|
||||
force = self.get_opt('force', False)
|
||||
offline = self.get_opt('offline', False)
|
||||
|
||||
if not offline:
|
||||
api_config = api_get_config(api_server)
|
||||
if not api_config:
|
||||
print "- the API server (%s) is not responding, please try again later." % api_server
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
role_name = args.pop(0).strip()
|
||||
role_name = self.args.pop(0).strip()
|
||||
if role_name == "":
|
||||
raise Exception("")
|
||||
raise AnsibleOptionsError("- no role name specified for init")
|
||||
role_path = os.path.join(init_path, role_name)
|
||||
if os.path.exists(role_path):
|
||||
if os.path.isfile(role_path):
|
||||
print "- the path %s already exists, but is a file - aborting" % role_path
|
||||
sys.exit(1)
|
||||
raise AnsibleError("- the path %s already exists, but is a file - aborting" % role_path)
|
||||
elif not force:
|
||||
print "- the directory %s already exists." % role_path
|
||||
print " you can use --force to re-initialize this directory,\n" + \
|
||||
raise AnsibleError("- the directory %s already exists." % role_path + \
|
||||
"you can use --force to re-initialize this directory,\n" + \
|
||||
"however it will reset any main.yml files that may have\n" + \
|
||||
" been modified there already."
|
||||
sys.exit(1)
|
||||
except Exception, e:
|
||||
parser.print_help()
|
||||
print "- no role name specified for init"
|
||||
sys.exit(1)
|
||||
|
||||
ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars')
|
||||
"been modified there already.")
|
||||
|
||||
# create the default README.md
|
||||
if not os.path.exists(role_path):
|
||||
|
@ -215,7 +195,7 @@ class Cli(object):
|
|||
f.write(default_readme_template)
|
||||
f.close
|
||||
|
||||
for dir in ROLE_DIRS:
|
||||
for dir in self.ROLE_DIRS:
|
||||
dir_path = os.path.join(init_path, role_name, dir)
|
||||
main_yml_path = os.path.join(dir_path, 'main.yml')
|
||||
# create the directory if it doesn't exist already
|
||||
|
@ -229,11 +209,11 @@ class Cli(object):
|
|||
# tags/platforms included (but commented out) and the
|
||||
# dependencies section
|
||||
platforms = []
|
||||
if not offline:
|
||||
platforms = api_get_list(api_server, "platforms") or []
|
||||
if not offline and self.api:
|
||||
platforms = self.api.get_list("platforms") or []
|
||||
categories = []
|
||||
if not offline:
|
||||
categories = api_get_list(api_server, "categories") or []
|
||||
if not offline and self.api:
|
||||
categories = self.api.get_list("categories") or []
|
||||
|
||||
# group the list of platforms from the api based
|
||||
# on their names, with the release field being
|
||||
|
@ -264,24 +244,20 @@ class Cli(object):
|
|||
f.close()
|
||||
print "- %s was created successfully" % role_name
|
||||
|
||||
def execute_info(args, options, parser):
|
||||
def execute_info(self):
|
||||
"""
|
||||
Executes the info action. This action prints out detailed
|
||||
information about an installed role as well as info available
|
||||
from the galaxy API.
|
||||
"""
|
||||
|
||||
if len(args) == 0:
|
||||
if len(self.args) == 0:
|
||||
# the user needs to specify a role
|
||||
parser.print_help()
|
||||
print "- you must specify a user/role name"
|
||||
sys.exit(1)
|
||||
raise AnsibleOptionsError("- you must specify a user/role name")
|
||||
|
||||
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
||||
api_config = api_get_config(api_server)
|
||||
roles_path = get_opt(options, "roles_path")
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
for role in args:
|
||||
for role in self.args:
|
||||
|
||||
role_info = {}
|
||||
|
||||
|
@ -292,11 +268,11 @@ class Cli(object):
|
|||
del install_info['version']
|
||||
role_info.update(install_info)
|
||||
|
||||
remote_data = api_lookup_role_by_name(api_server, role, False)
|
||||
remote_data = self.api.lookup_role_by_name(role, False)
|
||||
if remote_data:
|
||||
role_info.update(remote_data)
|
||||
|
||||
metadata = get_role_metadata(role, options)
|
||||
metadata = get_metadata(role, options)
|
||||
if metadata:
|
||||
role_info.update(metadata)
|
||||
|
||||
|
@ -322,7 +298,7 @@ class Cli(object):
|
|||
else:
|
||||
print "- the role %s was not found" % role
|
||||
|
||||
def execute_install(args, options, parser):
|
||||
def execute_install(self):
|
||||
"""
|
||||
Executes the installation action. The args list contains the
|
||||
roles to be installed, unless -f was specified. The list of roles
|
||||
|
@ -330,24 +306,19 @@ class Cli(object):
|
|||
or it can be a local .tar.gz file.
|
||||
"""
|
||||
|
||||
role_file = get_opt(options, "role_file", None)
|
||||
role_file = self.get_opt("role_file", None)
|
||||
|
||||
if len(args) == 0 and role_file is None:
|
||||
if len(self.args) == 0 and role_file is None:
|
||||
# the user needs to specify one of either --role-file
|
||||
# or specify a single user/role name
|
||||
parser.print_help()
|
||||
print "- you must specify a user/role name or a roles file"
|
||||
sys.exit()
|
||||
elif len(args) == 1 and not role_file is None:
|
||||
raise AnsibleOptionsError("- you must specify a user/role name or a roles file")
|
||||
elif len(self.args) == 1 and not role_file is None:
|
||||
# using a role file is mutually exclusive of specifying
|
||||
# the role name on the command line
|
||||
parser.print_help()
|
||||
print "- please specify a user/role name, or a roles file, but not both"
|
||||
sys.exit(1)
|
||||
raise AnsibleOptionsError("- please specify a user/role name, or a roles file, but not both")
|
||||
|
||||
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
||||
no_deps = get_opt(options, "no_deps", False)
|
||||
roles_path = get_opt(options, "roles_path")
|
||||
no_deps = self.get_opt("no_deps", False)
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
roles_done = []
|
||||
if role_file:
|
||||
|
@ -356,12 +327,12 @@ class Cli(object):
|
|||
roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f))
|
||||
else:
|
||||
# roles listed in a file, one per line
|
||||
roles_left = map(ansible.utils.role_spec_parse, f.readlines())
|
||||
roles_left = map(gr.get_spec, f.readlines())
|
||||
f.close()
|
||||
else:
|
||||
# roles were specified directly, so we'll just go out grab them
|
||||
# (and their dependencies, unless the user doesn't want us to).
|
||||
roles_left = map(ansible.utils.role_spec_parse, args)
|
||||
roles_left = map(gr.get_spec, self.args)
|
||||
|
||||
while len(roles_left) > 0:
|
||||
# query the galaxy API for the role data
|
||||
|
@ -387,19 +358,13 @@ class Cli(object):
|
|||
# just download a URL - version will probably be in the URL
|
||||
tmp_file = fetch_role(role_src, None, None, options)
|
||||
else:
|
||||
# installing from galaxy
|
||||
api_config = api_get_config(api_server)
|
||||
if not api_config:
|
||||
print "- the API server (%s) is not responding, please try again later." % api_server
|
||||
sys.exit(1)
|
||||
|
||||
role_data = api_lookup_role_by_name(api_server, role_src)
|
||||
role_data = self.api.lookup_role_by_name(role_src)
|
||||
if not role_data:
|
||||
print "- sorry, %s was not found on %s." % (role_src, api_server)
|
||||
print "- sorry, %s was not found on %s." % (role_src, self.options.api_server)
|
||||
exit_without_ignore(options)
|
||||
continue
|
||||
|
||||
role_versions = api_fetch_role_related(api_server, 'versions', role_data['id'])
|
||||
role_versions = self.api.fetch_role_related('versions', role_data['id'])
|
||||
if "version" not in role or role['version'] == '':
|
||||
# convert the version names to LooseVersion objects
|
||||
# and sort them to get the latest version. If there
|
||||
|
@ -430,7 +395,7 @@ class Cli(object):
|
|||
# install dependencies, if we want them
|
||||
if not no_deps and installed:
|
||||
if not role_data:
|
||||
role_data = get_role_metadata(role.get("name"), options)
|
||||
role_data = gr.get_metadata(role.get("name"), options)
|
||||
role_dependencies = role_data['dependencies']
|
||||
else:
|
||||
role_dependencies = role_data['summary_fields']['dependencies'] # api_fetch_role_related(api_server, 'dependencies', role_data['id'])
|
||||
|
@ -450,30 +415,28 @@ class Cli(object):
|
|||
if not tmp_file or not installed:
|
||||
print "- %s was NOT installed successfully." % role.get("name")
|
||||
exit_without_ignore(options)
|
||||
sys.exit(0)
|
||||
return 0
|
||||
|
||||
def execute_remove(args, options, parser):
|
||||
def execute_remove(self):
|
||||
"""
|
||||
Executes the remove action. The args list contains the list
|
||||
of roles to be removed. This list can contain more than one role.
|
||||
"""
|
||||
|
||||
if len(args) == 0:
|
||||
parser.print_help()
|
||||
print '- you must specify at least one role to remove.'
|
||||
sys.exit()
|
||||
if len(self.args) == 0:
|
||||
raise AnsibleOptionsError('- you must specify at least one role to remove.')
|
||||
|
||||
for role in args:
|
||||
for role in self.args:
|
||||
if get_role_metadata(role, options):
|
||||
if remove_role(role, options):
|
||||
print '- successfully removed %s' % role
|
||||
self.display.display('- successfully removed %s' % role)
|
||||
else:
|
||||
print "- failed to remove role: %s" % role
|
||||
self.display.display("- failed to remove role: %s" % role)
|
||||
else:
|
||||
print '- %s is not installed, skipping.' % role
|
||||
sys.exit(0)
|
||||
self.display.display('- %s is not installed, skipping.' % role)
|
||||
return 0
|
||||
|
||||
def execute_list(args, options, parser):
|
||||
def execute_list(self):
|
||||
"""
|
||||
Executes the list action. The args list can contain zero
|
||||
or one role. If one is specified, only that role will be
|
||||
|
@ -481,37 +444,33 @@ class Cli(object):
|
|||
be shown.
|
||||
"""
|
||||
|
||||
if len(args) > 1:
|
||||
print "- please specify only one role to list, or specify no roles to see a full list"
|
||||
sys.exit(1)
|
||||
if len(self.args) > 1:
|
||||
raise AnsibleOptionsError("- please specify only one role to list, or specify no roles to see a full list")
|
||||
|
||||
if len(args) == 1:
|
||||
if len(self.args) == 1:
|
||||
# show only the request role, if it exists
|
||||
role_name = args[0]
|
||||
metadata = get_role_metadata(role_name, options)
|
||||
role_name = self.args[0]
|
||||
gr = GalaxyRole(self.galaxy, role_name)
|
||||
metadata = gr.get_metadata()
|
||||
if metadata:
|
||||
install_info = get_galaxy_install_info(role_name, options)
|
||||
install_info = gr.get_galaxy_install_info()
|
||||
version = None
|
||||
if install_info:
|
||||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
# show some more info about single roles here
|
||||
print "- %s, %s" % (role_name, version)
|
||||
self.display.display("- %s, %s" % (role_name, version))
|
||||
else:
|
||||
print "- the role %s was not found" % role_name
|
||||
self.display.display("- the role %s was not found" % role_name)
|
||||
else:
|
||||
# show all valid roles in the roles_path directory
|
||||
roles_path = get_opt(options, 'roles_path')
|
||||
roles_path = self.get_opt('roles_path')
|
||||
roles_path = os.path.expanduser(roles_path)
|
||||
if not os.path.exists(roles_path):
|
||||
parser.print_help()
|
||||
print "- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path
|
||||
sys.exit(1)
|
||||
raise AnsibleOptionsError("- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path)
|
||||
elif not os.path.isdir(roles_path):
|
||||
print "- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
raise AnsibleOptionsError("- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path)
|
||||
path_files = os.listdir(roles_path)
|
||||
for path_file in path_files:
|
||||
if get_role_metadata(path_file, options):
|
||||
|
@ -521,40 +480,26 @@ class Cli(object):
|
|||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
print "- %s, %s" % (path_file, version)
|
||||
sys.exit(0)
|
||||
self.display.display("- %s, %s" % (path_file, version))
|
||||
return 0
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# The main entry point
|
||||
#-------------------------------------------------------------------------------------
|
||||
|
||||
#def main():
|
||||
# # parse the CLI options
|
||||
# action = get_action(sys.argv)
|
||||
# parser = build_option_parser(action)
|
||||
# (options, args) = parser.parse_args()
|
||||
#
|
||||
# # execute the desired action
|
||||
# if 1: #try:
|
||||
# fn = globals()["execute_%s" % action]
|
||||
# fn(args, options, parser)
|
||||
# #except KeyError, e:
|
||||
# # print "- error: %s is not a valid action. Valid actions are: %s" % (action, ", ".join(VALID_ACTIONS))
|
||||
# # sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
display = Display()
|
||||
|
||||
try:
|
||||
cli = Cli(display=display)
|
||||
cli.set_action(sys.argv)
|
||||
(options, args) = cli.parse()
|
||||
sys.exit(cli.run(options, args))
|
||||
except AnsibleError as e:
|
||||
display.error(str(e))
|
||||
cli = GalaxyCLI(sys.argv, display=display)
|
||||
cli.parse()
|
||||
sys.exit(cli.run())
|
||||
except AnsibleOptionsError as e:
|
||||
cli.parser.print_help()
|
||||
display.display(str(e), stderr=True, color='red')
|
||||
sys.exit(1)
|
||||
except AnsibleError as e:
|
||||
display.display(str(e), stderr=True, color='red')
|
||||
sys.exit(2)
|
||||
except KeyboardInterrupt:
|
||||
display.error("interrupted")
|
||||
sys.exit(1)
|
||||
sys.exit(3)
|
||||
|
|
|
@ -44,7 +44,7 @@ from ansible.parsing import DataLoader
|
|||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.playbook import Playbook
|
||||
from ansible.playbook.task import Task
|
||||
from ansible.utils.cli import base_parser, validate_conflicts, normalize_become_options, ask_passwords
|
||||
from ansible.utils.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.unicode import to_unicode
|
||||
from ansible.utils.vars import combine_vars
|
||||
|
@ -53,11 +53,13 @@ from ansible.vars import VariableManager
|
|||
|
||||
#---------------------------------------------------------------------------------------------------
|
||||
|
||||
def main(display, args):
|
||||
''' run ansible-playbook operations '''
|
||||
class PlaybookCLI(CLI):
|
||||
''' code behind ansible playbook cli'''
|
||||
|
||||
def parse(self):
|
||||
|
||||
# create parser for CLI options
|
||||
parser = base_parser(
|
||||
parser = CLI.base_parser(
|
||||
usage = "%prog playbook.yml",
|
||||
connect_opts=True,
|
||||
meta_opts=True,
|
||||
|
@ -77,35 +79,42 @@ def main(display, args):
|
|||
parser.add_option('--list-tags', dest='listtags', action='store_true',
|
||||
help="list all available tags")
|
||||
|
||||
options, args = parser.parse_args(args)
|
||||
self.options, self.args = parser.parse_args()
|
||||
|
||||
if len(args) == 0:
|
||||
if len(self.args) == 0:
|
||||
parser.print_help(file=sys.stderr)
|
||||
return 1
|
||||
raise AnsibleError("You must specify a playbook file to run")
|
||||
|
||||
display.verbosity = options.verbosity
|
||||
validate_conflicts(parser,options)
|
||||
self.parser = parser
|
||||
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
def run(self):
|
||||
|
||||
# Note: slightly wrong, this is written so that implicit localhost
|
||||
# Manage passwords
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
passwords = {}
|
||||
|
||||
# don't deal with privilege escalation when we don't need to
|
||||
if not options.listhosts and not options.listtasks and not options.listtags:
|
||||
normalize_become_options(options)
|
||||
(sshpass, becomepass, vault_pass) = ask_passwords(options)
|
||||
# don't deal with privilege escalation or passwords when we don't need to
|
||||
if not self.options.listhosts and not self.options.listtasks and not self.options.listtags:
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if options.vault_password_file:
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(options.vault_password_file)
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
|
||||
extra_vars = {}
|
||||
for extra_vars_opt in options.extra_vars:
|
||||
for extra_vars_opt in self.options.extra_vars:
|
||||
extra_vars_opt = to_unicode(extra_vars_opt, errors='strict')
|
||||
if extra_vars_opt.startswith(u"@"):
|
||||
# Argument is a YAML file (JSON is a subset of YAML)
|
||||
|
@ -119,14 +128,14 @@ def main(display, args):
|
|||
extra_vars = combine_vars(extra_vars, data)
|
||||
|
||||
# FIXME: this should be moved inside the playbook executor code
|
||||
only_tags = options.tags.split(",")
|
||||
skip_tags = options.skip_tags
|
||||
if options.skip_tags is not None:
|
||||
skip_tags = options.skip_tags.split(",")
|
||||
only_tags = self.options.tags.split(",")
|
||||
skip_tags = self.options.skip_tags
|
||||
if self.options.skip_tags is not None:
|
||||
skip_tags = self.ptions.skip_tags.split(",")
|
||||
|
||||
# initial error check, to make sure all specified playbooks are accessible
|
||||
# before we start running anything through the playbook executor
|
||||
for playbook in args:
|
||||
for playbook in self.args:
|
||||
if not os.path.exists(playbook):
|
||||
raise AnsibleError("the playbook: %s could not be found" % playbook)
|
||||
if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)):
|
||||
|
@ -138,7 +147,7 @@ def main(display, args):
|
|||
variable_manager.set_extra_vars(extra_vars)
|
||||
|
||||
# create the inventory, and filter it based on the subset specified (if any)
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=options.inventory)
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
variable_manager.set_inventory(inventory)
|
||||
|
||||
# (which is not returned in list_hosts()) is taken into account for
|
||||
|
@ -150,48 +159,50 @@ def main(display, args):
|
|||
no_hosts = False
|
||||
if len(inventory.list_hosts()) == 0:
|
||||
# Empty inventory
|
||||
display.warning("provided hosts list is empty, only localhost is available")
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
no_hosts = True
|
||||
inventory.subset(options.subset)
|
||||
inventory.subset(self.options.subset)
|
||||
if len(inventory.list_hosts()) == 0 and no_hosts is False:
|
||||
# Invalid limit
|
||||
raise errors.AnsibleError("Specified --limit does not match any hosts")
|
||||
raise AnsibleError("Specified --limit does not match any hosts")
|
||||
|
||||
# create the playbook executor, which manages running the plays via a task queue manager
|
||||
pbex = PlaybookExecutor(playbooks=args, inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=passwords)
|
||||
pbex = PlaybookExecutor(playbooks=self.args, inventory=inventory, variable_manager=variable_manager, loader=loader, display=self.display, options=self.options, passwords=passwords)
|
||||
|
||||
results = pbex.run()
|
||||
|
||||
if isinstance(results, list):
|
||||
for p in results:
|
||||
|
||||
display.display('\nplaybook: %s\n' % p['playbook'])
|
||||
self.display.display('\nplaybook: %s\n' % p['playbook'])
|
||||
for play in p['plays']:
|
||||
if options.listhosts:
|
||||
display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
|
||||
if self.options.listhosts:
|
||||
self.display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
|
||||
for host in play['hosts']:
|
||||
display.display(" %s" % host)
|
||||
if options.listtasks: #TODO: do we want to display block info?
|
||||
display.display("\n %s" % (play['name']))
|
||||
self.display.display(" %s" % host)
|
||||
if self.options.listtasks: #TODO: do we want to display block info?
|
||||
self.display.display("\n %s" % (play['name']))
|
||||
for task in play['tasks']:
|
||||
display.display(" %s" % task)
|
||||
if options.listtags: #TODO: fix once we figure out block handling above
|
||||
display.display("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
|
||||
self.display.display(" %s" % task)
|
||||
if self.options.listtags: #TODO: fix once we figure out block handling above
|
||||
self.display.display("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
|
||||
for tag in play['tags']:
|
||||
display.display(" %s" % tag)
|
||||
self.display.display(" %s" % tag)
|
||||
return 0
|
||||
else:
|
||||
return results
|
||||
|
||||
########################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
display = Display()
|
||||
#display.display(" ".join(sys.argv), log_only=True)
|
||||
|
||||
try:
|
||||
sys.exit(main(display, sys.argv[1:]))
|
||||
cli = PlaybookCLI(sys.argv, display=display)
|
||||
cli.parse()
|
||||
sys.exit(cli.run())
|
||||
except AnsibleError as e:
|
||||
display.error(str(e))
|
||||
display.display(str(e), stderr=True, color='red')
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
display.error("interrupted")
|
||||
|
|
|
@ -35,141 +35,100 @@ import traceback
|
|||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.parsing.vault import VaultEditor
|
||||
from ansible.utils.cli import base_parser, ask_vault_passwords
|
||||
from ansible.utils.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# Utility functions for parsing actions/options
|
||||
#-------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
class Cli(object):
|
||||
class VaultCli(CLI):
|
||||
""" Vault command line class """
|
||||
|
||||
VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view")
|
||||
CIPHER = 'AES256'
|
||||
|
||||
|
||||
def __init__(self, display=None):
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.vault_pass = None
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
|
||||
super(VaultCli, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
|
||||
# create parser for CLI options
|
||||
parser = base_parser(
|
||||
self.parser = CLI.base_parser(
|
||||
usage = "%prog vaultfile.yml",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
self.set_action()
|
||||
|
||||
def run(self, options, args):
|
||||
|
||||
action = self.get_action(args)
|
||||
|
||||
if not action:
|
||||
parser.print_help()
|
||||
raise AnsibleError("missing required action")
|
||||
|
||||
# options specific to actions
|
||||
if action == "create":
|
||||
parser.set_usage("usage: %prog create [options] file_name")
|
||||
elif action == "decrypt":
|
||||
parser.set_usage("usage: %prog decrypt [options] file_name")
|
||||
elif action == "edit":
|
||||
parser.set_usage("usage: %prog edit [options] file_name")
|
||||
elif action == "view":
|
||||
parser.set_usage("usage: %prog view [options] file_name")
|
||||
elif action == "encrypt":
|
||||
parser.set_usage("usage: %prog encrypt [options] file_name")
|
||||
# options specific to self.actions
|
||||
if self.action == "create":
|
||||
self.parser.set_usage("usage: %prog create [options] file_name")
|
||||
elif self.action == "decrypt":
|
||||
self.parser.set_usage("usage: %prog decrypt [options] file_name")
|
||||
elif self.action == "edit":
|
||||
self.parser.set_usage("usage: %prog edit [options] file_name")
|
||||
elif self.action == "view":
|
||||
self.parser.set_usage("usage: %prog view [options] file_name")
|
||||
elif self.action == "encrypt":
|
||||
self.parser.set_usage("usage: %prog encrypt [options] file_name")
|
||||
elif action == "rekey":
|
||||
parser.set_usage("usage: %prog rekey [options] file_name")
|
||||
self.parser.set_usage("usage: %prog rekey [options] file_name")
|
||||
|
||||
if len(args) == 0 or len(args) > 1:
|
||||
parser.print_help()
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
|
||||
if len(self.args) == 0 or len(self.args) > 1:
|
||||
self.parser.print_help()
|
||||
raise AnsibleError("Vault requires a single filename as a parameter")
|
||||
|
||||
if options.vault_password_file:
|
||||
def run(self):
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
self.vault_pass = read_vault_file(options.vault_password_file)
|
||||
else:
|
||||
self.vault_pass, _= ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
|
||||
self.vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
self.vault_pass, _= self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
|
||||
|
||||
# execute the desired action
|
||||
fn = getattr(self, "execute_%s" % action)
|
||||
fn(args, options)
|
||||
self.execute()
|
||||
|
||||
def get_action(self, args):
|
||||
"""
|
||||
Get the action the user wants to execute from the
|
||||
sys argv list.
|
||||
"""
|
||||
for i in range(0,len(args)):
|
||||
arg = args[i]
|
||||
if arg in VALID_ACTIONS:
|
||||
del args[i]
|
||||
return arg
|
||||
return None
|
||||
def execute_create(self):
|
||||
|
||||
def execute_create(args, options):
|
||||
|
||||
cipher = 'AES256'
|
||||
if hasattr(options, 'cipher'):
|
||||
cipher = options.cipher
|
||||
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, args[0])
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, self.args[0])
|
||||
this_editor.create_file()
|
||||
|
||||
def execute_decrypt(args, options):
|
||||
def execute_decrypt(self):
|
||||
|
||||
cipher = 'AES256'
|
||||
if hasattr(options, 'cipher'):
|
||||
cipher = options.cipher
|
||||
|
||||
for f in args:
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.decrypt_file()
|
||||
|
||||
self.display.display("Decryption successful")
|
||||
|
||||
def execute_edit(args, options):
|
||||
def execute_edit(self):
|
||||
|
||||
cipher = None
|
||||
|
||||
for f in args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.edit_file()
|
||||
|
||||
def execute_view(args, options):
|
||||
def execute_view(self):
|
||||
|
||||
cipher = None
|
||||
|
||||
for f in args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.view_file()
|
||||
|
||||
def execute_encrypt(args, options):
|
||||
def execute_encrypt(self):
|
||||
|
||||
cipher = 'AES256'
|
||||
if hasattr(options, 'cipher'):
|
||||
cipher = options.cipher
|
||||
|
||||
for f in args:
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.encrypt_file()
|
||||
|
||||
self.display.display("Encryption successful")
|
||||
|
||||
def execute_rekey(args, options ):
|
||||
__, new_password = ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
|
||||
def execute_rekey(self):
|
||||
__, new_password = self.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
|
||||
|
||||
cipher = None
|
||||
for f in args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.rekey_file(new_password)
|
||||
|
||||
self.display.display("Rekey successful")
|
||||
|
@ -179,14 +138,12 @@ class Cli(object):
|
|||
if __name__ == "__main__":
|
||||
|
||||
display = Display()
|
||||
#display.display(" ".join(sys.argv), log_only=True)
|
||||
|
||||
try:
|
||||
cli = Cli(display=display)
|
||||
(options, args) = cli.parse()
|
||||
sys.exit(cli.run(options, args))
|
||||
cli = VaultCli(sys.argv, display=display)
|
||||
cli.parse()
|
||||
sys.exit(cli.run())
|
||||
except AnsibleError as e:
|
||||
display.error(str(e))
|
||||
display.display(str(e), stderr=True, color='red')
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
display.error("interrupted")
|
||||
|
|
Loading…
Reference in a new issue