Merge pull request #463 from darko-poljak/socket-file-fix

Socket file fix
This commit is contained in:
Nico Schottelius 2016-07-25 11:43:04 +02:00 committed by GitHub
commit 1a4adc0e21
14 changed files with 246 additions and 164 deletions

View File

@ -246,3 +246,6 @@ pub:
test:
$(helper) $@
pep8:
$(helper) $@

View File

@ -249,7 +249,7 @@ eof
# First check everything is sane
"$0" check-date
"$0" check-unittest
"$0" pep8
"$0" check-pep8
# Generate version file to be included in packaging
"$0" target-version
@ -360,7 +360,11 @@ eof
;;
pep8)
pep8 ${basedir} | less
pep8 "${basedir}" "${basedir}/scripts/cdist" | less
;;
check-pep8)
"$0" pep8
echo "Please review pep8 report."
while true
do

View File

@ -284,7 +284,7 @@ eof
# First check everything is sane
"$0" check-date
"$0" check-unittest
"$0" pep8
"$0" check-pep8
# Generate version file to be included in packaging
"$0" target-version
@ -422,7 +422,11 @@ eof
;;
pep8)
pep8 ${basedir} | less
pep8 "${basedir}" "${basedir}/scripts/cdist" | less
;;
check-pep8)
"$0" pep8
echo "Please review pep8 report."
while true
do

View File

@ -21,6 +21,7 @@
import os
import subprocess
import hashlib
import cdist.version
@ -82,3 +83,11 @@ def file_to_list(filename):
lines = []
return lines
def str_hash(s):
"""Return hash of string s"""
if isinstance(s, str):
return hashlib.md5(s.encode('utf-8')).hexdigest()
else:
raise Error("Param should be string")

View File

@ -27,6 +27,7 @@ import sys
import time
import pprint
import itertools
import tempfile
import cdist
@ -36,6 +37,36 @@ import cdist.exec.remote
from cdist import core
def inspect_ssh_mux_opts():
"""Inspect whether or not ssh supports multiplexing options.
Return string containing multiplexing options if supported.
If ControlPath is supported then placeholder for that path is
specified and can be used for final string formatting.
For example, this function can return string:
"-o ControlMaster=auto -o ControlPersist=125 -o ControlPath={}".
Then it can be formatted:
mux_opts_string.format('/tmp/tmpxxxxxx/ssh-control-path').
"""
import subprocess
wanted_mux_opts = {
"ControlPath": "{}",
"ControlMaster": "auto",
"ControlPersist": "125",
}
mux_opts = " ".join([" -o {}={}".format(
x, wanted_mux_opts[x]) for x in wanted_mux_opts])
try:
subprocess.check_output("ssh {}".format(mux_opts),
stderr=subprocess.STDOUT, shell=True)
except subprocess.CalledProcessError as e:
subproc_output = e.output.decode().lower()
if "bad configuration option" in subproc_output:
return ""
return mux_opts
class Config(object):
"""Cdist main class to hold arbitrary data"""
@ -94,7 +125,6 @@ class Config(object):
initial_manifest_tempfile = None
if args.manifest == '-':
# read initial manifest from stdin
import tempfile
try:
handle, initial_manifest_temp_path = tempfile.mkstemp(
prefix='cdist.stdin.')
@ -112,18 +142,47 @@ class Config(object):
failed_hosts = []
time_start = time.time()
# default remote cmd patterns
args.remote_exec_pattern = None
args.remote_copy_pattern = None
args_dict = vars(args)
# if remote-exec and/or remote-copy args are None then user
# didn't specify command line options nor env vars:
# inspect multiplexing options for default cdist.REMOTE_COPY/EXEC
if (args_dict['remote_copy'] is None or
args_dict['remote_exec'] is None):
mux_opts = inspect_ssh_mux_opts()
if args_dict['remote_exec'] is None:
args.remote_exec_pattern = cdist.REMOTE_EXEC + mux_opts
if args_dict['remote_copy'] is None:
args.remote_copy_pattern = cdist.REMOTE_COPY + mux_opts
if args.out_path:
base_root_path = args.out_path
else:
base_root_path = tempfile.mkdtemp()
hostcnt = 0
for host in itertools.chain(cls.hosts(args.host),
cls.hosts(args.hostfile)):
hostdir = cdist.str_hash(host)
host_base_path = os.path.join(base_root_path, hostdir)
log.debug("Base root path for target host \"{}\" is \"{}\"".format(
host, host_base_path))
hostcnt += 1
if args.parallel:
log.debug("Creating child process for %s", host)
process[host] = multiprocessing.Process(
target=cls.onehost, args=(host, args, True))
target=cls.onehost,
args=(host, host_base_path, hostdir, args, True))
process[host].start()
else:
try:
cls.onehost(host, args, parallel=False)
cls.onehost(host, host_base_path, hostdir,
args, parallel=False)
except cdist.Error as e:
failed_hosts.append(host)
@ -145,22 +204,42 @@ class Config(object):
" ".join(failed_hosts))
@classmethod
def onehost(cls, host, args, parallel):
def onehost(cls, host, host_base_path, host_dir_name, args, parallel):
"""Configure ONE system"""
log = logging.getLogger(host)
try:
control_path = os.path.join(host_base_path, "ssh-control-path")
# If we constructed patterns for remote commands then there is
# placeholder for ssh ControlPath, format it and we have unique
# ControlPath for each host.
#
# If not then use args.remote_exec/copy that user specified.
if args.remote_exec_pattern:
remote_exec = args.remote_exec_pattern.format(control_path)
else:
remote_exec = args.remote_exec
if args.remote_copy_pattern:
remote_copy = args.remote_copy_pattern.format(control_path)
else:
remote_copy = args.remote_copy
log.debug("remote_exec for host \"{}\": {}".format(
host, remote_exec))
log.debug("remote_copy for host \"{}\": {}".format(
host, remote_copy))
local = cdist.exec.local.Local(
target_host=host,
base_root_path=host_base_path,
host_dir_name=host_dir_name,
initial_manifest=args.manifest,
base_path=args.out_path,
add_conf_dirs=args.conf_dir)
remote = cdist.exec.remote.Remote(
target_host=host,
remote_exec=args.remote_exec,
remote_copy=args.remote_copy)
remote_exec=remote_exec,
remote_copy=remote_copy)
c = cls(local, remote, dry_run=args.dry_run)
c.run()

View File

@ -29,7 +29,6 @@ import subprocess
import shutil
import logging
import tempfile
import hashlib
import cdist
import cdist.message
@ -48,40 +47,25 @@ class Local(object):
"""
def __init__(self,
target_host,
base_root_path,
host_dir_name,
exec_path=sys.argv[0],
initial_manifest=None,
base_path=None,
add_conf_dirs=None):
self.target_host = target_host
self._init_log()
# FIXME: stopped: create base that does not require moving later
if base_path:
base_path_parent = base_path
else:
base_path_parent = tempfile.mkdtemp()
# TODO: the below atexit hook nukes any debug info we would have
# if cdist exits with error.
# import atexit
# atexit.register(lambda: shutil.rmtree(base_path_parent))
self.hostdir = self._hostdir()
self.log.info("Calculated temp dir for target \"{}\" is "
"\"{}\"".format(self.target_host, self.hostdir))
self.base_path = os.path.join(base_path_parent, self.hostdir)
self._init_permissions()
self.mkdir(self.base_path)
# FIXME: as well
self._init_cache_dir(None)
self.hostdir = host_dir_name
self.base_path = os.path.join(base_root_path, "data")
self.exec_path = exec_path
self.custom_initial_manifest = initial_manifest
self._add_conf_dirs = add_conf_dirs
self._init_log()
self._init_permissions()
self.mkdir(self.base_path)
# FIXME: create dir that does not require moving later
self._init_cache_dir(None)
self._init_paths()
self._init_object_marker()
self._init_conf_dirs()
@ -98,12 +82,6 @@ class Local(object):
else:
return None
def _hostdir(self):
# Do not assume target_host is anything that can be used as a
# directory name.
# Instead use a hash, which is known to work as directory name.
return hashlib.md5(self.target_host.encode('utf-8')).hexdigest()
def _init_log(self):
self.log = logging.getLogger(self.target_host)

View File

@ -41,10 +41,13 @@ class CodeTestCase(test.CdistTestCase):
def setUp(self):
self.local_dir = self.mkdtemp()
self.hostdir = cdist.str_hash(self.target_host)
self.host_base_path = os.path.join(self.local_dir, self.hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=self.local_dir,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
exec_path=cdist.test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()

View File

@ -55,10 +55,13 @@ class ConfigRunTestCase(test.CdistTestCase):
self.temp_dir = self.mkdtemp()
self.local_dir = os.path.join(self.temp_dir, "local")
os.mkdir(self.local_dir)
self.hostdir = cdist.str_hash(self.target_host)
self.host_base_path = os.path.join(self.local_dir, self.hostdir)
os.makedirs(self.host_base_path)
self.local = cdist.exec.local.Local(
target_host=self.target_host,
base_path=self.local_dir)
base_root_path=self.host_base_path,
host_dir_name=self.hostdir)
# Setup test objects
self.object_base_path = op.join(self.temp_dir, 'object')
@ -161,7 +164,8 @@ class ConfigRunTestCase(test.CdistTestCase):
"""Test if the dryrun option is working like expected"""
drylocal = cdist.exec.local.Local(
target_host=self.target_host,
base_path=self.local_dir,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
# exec_path can not derivated from sys.argv in case of unittest
exec_path=os.path.abspath(os.path.join(
my_dir, '../../../scripts/cdist')),
@ -184,3 +188,8 @@ class ConfigRunTestCase(test.CdistTestCase):
# first.requirements = ['__singleton_test/foo']
# with self.assertRaises(cdist.core.?????):
# self.config.iterate_until_finished()
if __name__ == "__main__":
import unittest
unittest.main()

View File

@ -48,10 +48,13 @@ class EmulatorTestCase(test.CdistTestCase):
handle, self.script = self.mkstemp(dir=self.temp_dir)
os.close(handle)
base_path = self.temp_dir
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()
@ -148,10 +151,13 @@ class EmulatorConflictingRequirementsTestCase(test.CdistTestCase):
handle, self.script = self.mkstemp(dir=self.temp_dir)
os.close(handle)
base_path = self.temp_dir
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()
@ -235,10 +241,13 @@ class AutoRequireEmulatorTestCase(test.CdistTestCase):
def setUp(self):
self.temp_dir = self.mkdtemp()
base_path = os.path.join(self.temp_dir, "out")
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()
@ -265,10 +274,13 @@ class OverrideTestCase(test.CdistTestCase):
handle, self.script = self.mkstemp(dir=self.temp_dir)
os.close(handle)
base_path = self.temp_dir
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()
@ -303,12 +315,15 @@ class ArgumentsTestCase(test.CdistTestCase):
def setUp(self):
self.temp_dir = self.mkdtemp()
base_path = self.temp_dir
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
handle, self.script = self.mkstemp(dir=self.temp_dir)
os.close(handle)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()
@ -425,10 +440,13 @@ class StdinTestCase(test.CdistTestCase):
self.temp_dir = self.mkdtemp()
base_path = os.path.join(self.temp_dir, "out")
hostdir = cdist.str_hash(self.target_host)
host_base_path = os.path.join(base_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=base_path,
base_root_path=host_base_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir])

View File

@ -47,11 +47,14 @@ class LocalTestCase(test.CdistTestCase):
target_host = 'localhost'
self.temp_dir = self.mkdtemp()
self.out_parent_path = self.temp_dir
self.out_path = op.join(self.out_parent_path, target_host)
self.hostdir = cdist.str_hash(target_host)
self.host_base_path = op.join(self.out_parent_path, self.hostdir)
self.out_path = op.join(self.host_base_path, "data")
self.local = local.Local(
target_host=target_host,
base_path=self.out_parent_path,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
exec_path=test.cdist_exec_path
)
@ -109,7 +112,8 @@ class LocalTestCase(test.CdistTestCase):
link_test_local = local.Local(
target_host='localhost',
base_path=self.out_parent_path,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
exec_path=test.cdist_exec_path,
)
@ -127,7 +131,8 @@ class LocalTestCase(test.CdistTestCase):
link_test_local = local.Local(
target_host='localhost',
base_path=self.out_parent_path,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir]
)
@ -148,7 +153,8 @@ class LocalTestCase(test.CdistTestCase):
link_test_local = local.Local(
target_host='localhost',
base_path=self.out_parent_path,
base_root_path=self.host_base_path,
host_dir_name=self.hostdir,
exec_path=test.cdist_exec_path,
)

View File

@ -42,12 +42,15 @@ class ExplorerClassTestCase(test.CdistTestCase):
def setUp(self):
self.temp_dir = self.mkdtemp()
self.local_path = os.path.join(self.temp_dir, "local")
hostdir = cdist.str_hash(self.target_host)
base_root_path = os.path.join(self.local_path, hostdir)
self.remote_base_path = os.path.join(self.temp_dir, "remote")
os.makedirs(self.remote_base_path)
self.local = local.Local(
target_host=self.target_host,
base_path=self.local_path,
base_root_path=base_root_path,
host_dir_name=hostdir,
exec_path=test.cdist_exec_path,
add_conf_dirs=[conf_dir],
)

View File

@ -49,9 +49,12 @@ class ManifestTestCase(test.CdistTestCase):
self.temp_dir = self.mkdtemp()
out_path = self.temp_dir
hostdir = cdist.str_hash(self.target_host)
base_root_path = os.path.join(out_path, hostdir)
self.local = local.Local(
target_host=self.target_host,
base_path=out_path,
base_root_path=base_root_path,
host_dir_name=hostdir,
exec_path=cdist.test.cdist_exec_path,
add_conf_dirs=[conf_dir])
self.local.create_files_dirs()

View File

@ -2,6 +2,7 @@ Changelog
---------
next:
* Core: Fix ssh ControlPath socket file error (Darko Poljak)
* Documentation: Update cdist man page and cdist-references (Darko Poljak)
* Documentation: Change cdist and cdist-type__pyvenv man page licenses to GPLv3+ (Darko Poljak)
* Documentation: Add FILES to cdist man page (Darko Poljak)

View File

@ -21,31 +21,6 @@
#
#
def inspect_ssh_mux_opts(control_path_dir="~/.ssh/"):
"""Inspect whether or not ssh supports multiplexing options"""
import subprocess
import os
# socket is always local to each cdist run, it is created in
# temp directory that is created at starting cdist, so
# control_path file name can be static, it will always be
# unique due to unique temp directory name
control_path = os.path.join(control_path_dir, "ssh-control-path")
wanted_mux_opts = {
"ControlPath": control_path,
"ControlMaster": "auto",
"ControlPersist": "125",
}
mux_opts = " ".join([" -o {}={}".format(x,
wanted_mux_opts[x]) for x in wanted_mux_opts])
try:
subprocess.check_output("ssh {}".format(mux_opts),
stderr=subprocess.STDOUT, shell=True)
except subprocess.CalledProcessError as e:
subproc_output = e.output.decode().lower()
if "bad configuration option" in subproc_output:
return ""
return mux_opts
def commandline():
"""Parse command line"""
@ -54,7 +29,6 @@ def commandline():
import cdist.banner
import cdist.config
import cdist.shell
import tempfile
import shutil
import os
@ -62,79 +36,88 @@ def commandline():
parser = {}
# Options _all_ parsers have in common
parser['loglevel'] = argparse.ArgumentParser(add_help=False)
parser['loglevel'].add_argument('-d', '--debug',
help='Set log level to debug', action='store_true',
default=False)
parser['loglevel'].add_argument('-v', '--verbose',
help='Set log level to info, be more verbose',
action='store_true', default=False)
parser['loglevel'].add_argument(
'-d', '--debug', help='Set log level to debug',
action='store_true', default=False)
parser['loglevel'].add_argument(
'-v', '--verbose', help='Set log level to info, be more verbose',
action='store_true', default=False)
# Main subcommand parser
parser['main'] = argparse.ArgumentParser(description='cdist '
+ cdist.VERSION,
parents=[parser['loglevel']])
parser['main'].add_argument('-V', '--version',
help='Show version', action='version',
version='%(prog)s ' + cdist.VERSION)
parser['sub'] = parser['main'].add_subparsers(title="Commands",
dest="command")
parser['main'] = argparse.ArgumentParser(
description='cdist ' + cdist.VERSION, parents=[parser['loglevel']])
parser['main'].add_argument(
'-V', '--version', help='Show version', action='version',
version='%(prog)s ' + cdist.VERSION)
parser['sub'] = parser['main'].add_subparsers(
title="Commands", dest="command")
# Banner
parser['banner'] = parser['sub'].add_parser('banner',
parents=[parser['loglevel']])
parser['banner'] = parser['sub'].add_parser(
'banner', parents=[parser['loglevel']])
parser['banner'].set_defaults(func=cdist.banner.banner)
# Config
parser['config'] = parser['sub'].add_parser('config',
parents=[parser['loglevel']])
parser['config'].add_argument('host', nargs='*',
help='host(s) to operate on')
parser['config'].add_argument('-c', '--conf-dir',
help=('Add configuration directory (can be repeated, '
'last one wins)'), action='append')
parser['config'].add_argument('-f', '--file',
help=('Read additional hosts to operate on from specified file '
'or from stdin if \'-\' (each host on separate line). '
'If no host or host file is specified then, by default, '
'read hosts from stdin.'),
dest='hostfile', required=False)
parser['config'].add_argument('-i', '--initial-manifest',
help='Path to a cdist manifest or \'-\' to read from stdin.',
dest='manifest', required=False)
parser['config'].add_argument('-n', '--dry-run',
help='Do not execute code', action='store_true')
parser['config'].add_argument('-o', '--out-dir',
help='Directory to save cdist output in', dest="out_path")
parser['config'].add_argument('-p', '--parallel',
help='Operate on multiple hosts in parallel',
action='store_true', dest='parallel')
parser['config'].add_argument('-s', '--sequential',
help='Operate on multiple hosts sequentially (default)',
action='store_false', dest='parallel')
parser['config'] = parser['sub'].add_parser(
'config', parents=[parser['loglevel']])
parser['config'].add_argument(
'host', nargs='*', help='host(s) to operate on')
parser['config'].add_argument(
'-c', '--conf-dir',
help=('Add configuration directory (can be repeated, '
'last one wins)'), action='append')
parser['config'].add_argument(
'-f', '--file',
help=('Read additional hosts to operate on from specified file '
'or from stdin if \'-\' (each host on separate line). '
'If no host or host file is specified then, by default, '
'read hosts from stdin.'),
dest='hostfile', required=False)
parser['config'].add_argument(
'-i', '--initial-manifest',
help='Path to a cdist manifest or \'-\' to read from stdin.',
dest='manifest', required=False)
parser['config'].add_argument(
'-n', '--dry-run',
help='Do not execute code', action='store_true')
parser['config'].add_argument(
'-o', '--out-dir',
help='Directory to save cdist output in', dest="out_path")
parser['config'].add_argument(
'-p', '--parallel',
help='Operate on multiple hosts in parallel',
action='store_true', dest='parallel')
parser['config'].add_argument(
'-s', '--sequential',
help='Operate on multiple hosts sequentially (default)',
action='store_false', dest='parallel')
# remote-copy and remote-exec defaults are environment variables
# if set; if not then None - these will be futher handled after
# parsing to determine implementation default
parser['config'].add_argument('--remote-copy',
help='Command to use for remote copy (should behave like scp)',
action='store', dest='remote_copy',
default=os.environ.get('CDIST_REMOTE_COPY'))
parser['config'].add_argument('--remote-exec',
help=('Command to use for remote execution '
'(should behave like ssh)'),
action='store', dest='remote_exec',
default=os.environ.get('CDIST_REMOTE_EXEC'))
parser['config'].add_argument(
'--remote-copy',
help='Command to use for remote copy (should behave like scp)',
action='store', dest='remote_copy',
default=os.environ.get('CDIST_REMOTE_COPY'))
parser['config'].add_argument(
'--remote-exec',
help=('Command to use for remote execution '
'(should behave like ssh)'),
action='store', dest='remote_exec',
default=os.environ.get('CDIST_REMOTE_EXEC'))
parser['config'].set_defaults(func=cdist.config.Config.commandline)
# Shell
parser['shell'] = parser['sub'].add_parser('shell',
parents=[parser['loglevel']])
parser['shell'].add_argument('-s', '--shell',
help='Select shell to use, defaults to current shell')
parser['shell'] = parser['sub'].add_parser(
'shell', parents=[parser['loglevel']])
parser['shell'].add_argument(
'-s', '--shell',
help='Select shell to use, defaults to current shell')
parser['shell'].set_defaults(func=cdist.shell.Shell.commandline)
for p in parser:
parser[p].epilog = "Get cdist at http://www.nico.schottelius.org/software/cdist/"
parser[p].epilog = (
"Get cdist at http://www.nico.schottelius.org/software/cdist/")
args = parser['main'].parse_args(sys.argv[1:])
@ -143,26 +126,6 @@ def commandline():
logging.root.setLevel(logging.INFO)
if args.debug:
logging.root.setLevel(logging.DEBUG)
args_dict = vars(args)
# if command with remote_copy and remote_exec params
if 'remote_copy' in args_dict and 'remote_exec' in args_dict:
# if remote-exec and/or remote-copy args are None then user
# didn't specify command line options nor env vars:
# inspect multiplexing options for default cdist.REMOTE_COPY/EXEC
if args_dict['remote_copy'] is None or args_dict['remote_exec'] is None:
control_path_dir = tempfile.mkdtemp(prefix="cdist")
import atexit
atexit.register(lambda: shutil.rmtree(control_path_dir))
mux_opts = inspect_ssh_mux_opts(control_path_dir)
if args_dict['remote_exec'] is None:
args.remote_exec = cdist.REMOTE_EXEC + mux_opts
if args_dict['remote_copy'] is None:
args.remote_copy = cdist.REMOTE_COPY + mux_opts
if args.command == 'config':
if args.manifest == '-' and args.hostfile == '-':
print('cdist config: error: cannot read both, manifest and host file, from stdin')
sys.exit(1)
log.debug(args)
log.info("version %s" % cdist.VERSION)
@ -191,10 +154,9 @@ if __name__ == "__main__":
cdistpythonversion = '3.2'
if sys.version < cdistpythonversion:
print('Python >= ' + cdistpythonversion +
' is required on the source host.', file=sys.stderr)
' is required on the source host.', file=sys.stderr)
sys.exit(1)
exit_code = 0
try: