Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F60237218
epfl_roaming.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sun, Apr 28, 13:27
Size
40 KB
Mime Type
text/x-python
Expires
Tue, Apr 30, 13:27 (2 d)
Engine
blob
Format
Raw Data
Handle
17323020
Attached To
R8811 EPFL Roaming
epfl_roaming.py
View Options
#!/usr/bin/env python
# -*- coding: utf-8 -*-
###
# Bancal Samuel
# 121002
# 121105 :
# GConf can't make it work! Giving up for now.
###
# Usage :
#
# - epfl_roaming.py --pam
# trigged by PAM at session_open & session_close
# run as root
# does :
# - mount & umount
# - files/folders ln -s & cp
# - rm -rf at session_close
# - GConf dump (Disabled) at session_close
# - DConf dump at session_close
#
# - epfl_roaming.py --session
# trigged at Gnome/Unity new session
# as user
# does :
# - GConf load (Disabled)
# - DConf load
#
# - epfl_roaming.py --on_halt
# trigged by /etc/init/epfl_roaming.conf at shutdown/reboot
# does :
# - run roaming_close for every user still logged
#
# - epfl_roaming.py --torque_prologue
# trigged by /var/spool/torque/mom_priv/prologue
# does :
# - empty home dir for user
#
# - epfl_roaming.py --torque_epilogue
# trigged by /var/spool/torque/mom_priv/epilogue
# does :
# - simple remove home dir if not other session for that user
#
###
# Requires :
# sudo apt install python-lockfile
import
os
import
sys
import
re
import
argparse
import
pwd
,
grp
import
ldap
import
pickle
import
subprocess
# import pprint
import
lockfile
import
shutil
import
xml.dom.minidom
import
datetime
import
signal
import
time
import
traceback
### CONSTANTS
LOG_PAM
=
"/var/log/epfl_roaming.log"
LOG_SESSION
=
"/tmp/epfl_roaming_{username}.log"
# username replaced during execution time
CONFIG_FILE
=
"/usr/local/etc/epfl_roaming.conf"
LDAP_SERVER
=
"ldap://ldap.epfl.ch"
LDAP_BASE_DN
=
"c=ch"
LDAP_SCOPE
=
ldap
.
SCOPE_SUBTREE
LDAP_NB_RETRY
=
3
RM_MAX_ATTEMPT
=
3
RM_SLEEP
=
1
UMOUNT_MAX_ATTEMPT
=
3
UMOUNT_SLEEP
=
1
UNLINK_CRED_FILE
=
True
SEMAPHORE_LOCK_FILE
=
"/tmp/epfl_roaming_global_lock"
class
PreventInterrupt
(
object
):
def
__init__
(
self
):
pass
def
__enter__
(
self
):
PreventInterrupt
.
__no_interrupt__
()
def
__exit__
(
self
,
typ
,
val
,
tb
):
PreventInterrupt
.
__can_interrupt__
()
@classmethod
def
is_interruptible
(
cls
):
try
:
return
cls
.
__can_interrupt
except
Exception
:
return
True
@classmethod
def
__no_interrupt__
(
cls
):
cls
.
__can_interrupt
=
False
@classmethod
def
__can_interrupt__
(
cls
):
cls
.
__can_interrupt
=
True
class
UserIdentity
():
def
__init__
(
self
,
user
):
self
.
user
=
user
def
__enter__
(
self
):
os
.
setegid
(
int
(
self
.
user
.
gid
))
os
.
seteuid
(
int
(
self
.
user
.
uid
))
IO
.
write
(
"ID changed :
%s
"
%
(
os
.
getresuid
(),
))
def
__exit__
(
self
,
typ
,
val
,
tb
):
os
.
seteuid
(
0
)
os
.
setegid
(
0
)
IO
.
write
(
"ID changed :
%s
"
%
(
os
.
getresuid
(),
))
class
IO
(
object
):
def
__init__
(
self
,
filename
):
self
.
filename
=
filename
def
__enter__
(
self
):
IO
.
__open__
(
self
.
filename
)
try
:
for
msg
,
eol
in
IO
.
previous_writes
:
IO
.
write
(
msg
,
eol
)
except
AttributeError
:
pass
def
__exit__
(
self
,
typ
,
val
,
tb
):
IO
.
__close__
()
@classmethod
def
write
(
cls
,
msg
,
eol
=
"
\n
"
):
pid
=
os
.
getpid
()
try
:
cls
.
f
.
write
(
"
\n
"
.
join
([
"(
%s
)
%s
"
%
(
pid
,
s
)
for
s
in
msg
.
split
(
"
\n
"
)])
+
eol
)
except
AttributeError
:
try
:
cls
.
previous_writes
.
append
((
msg
,
eol
))
except
AttributeError
:
cls
.
previous_writes
=
[(
msg
,
eol
),]
@classmethod
def
__open__
(
cls
,
filename
):
cls
.
f
=
open
(
filename
,
"a"
,
1
)
# line buffered
@classmethod
def
__close__
(
cls
):
cls
.
f
.
close
()
class
NameSpace
(
object
):
def
__repr__
(
self
):
type_name
=
type
(
self
)
.
__name__
args_string
=
[]
for
arg
in
self
.
_get_args
():
args_string
.
append
(
repr
(
arg
))
for
name
,
value
in
self
.
_get_kwargs
():
args_string
.
append
(
"
%s
=
%r
"
%
(
name
,
value
))
return
"
%s
(
%s
)"
%
(
type_name
,
", "
.
join
(
args_string
))
def
_get_kwargs
(
self
):
return
sorted
(
self
.
__dict__
.
items
())
def
_get_args
(
self
):
return
[]
class
Ldap
(
object
):
def
__init__
(
self
):
success
=
False
for
_
in
xrange
(
LDAP_NB_RETRY
):
try
:
self
.
l
=
ldap
.
initialize
(
LDAP_SERVER
)
success
=
True
except
Exception
,
e
:
time
.
sleep
(
1
)
if
not
success
:
raise
e
def
search_s
(
self
,
l_filter
,
l_attrs
):
for
_
in
xrange
(
LDAP_NB_RETRY
):
try
:
return
self
.
l
.
search_s
(
base
=
LDAP_BASE_DN
,
scope
=
LDAP_SCOPE
,
filterstr
=
l_filter
,
attrlist
=
l_attrs
)
except
Exception
,
e
:
time
.
sleep
(
1
)
raise
e
def
run_cmd
(
cmd
,
s_cmd
=
None
,
env
=
None
,
stdin
=
None
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
STDOUT
,
s_input
=
None
,
shell
=
False
):
p
=
subprocess
.
Popen
(
cmd
,
env
=
env
,
stdin
=
stdin
,
stdout
=
stdout
,
stderr
=
stderr
,
shell
=
shell
,
)
if
s_cmd
!=
None
:
IO
.
write
(
"-> (
%s
)
%s
"
%
(
p
.
pid
,
s_cmd
))
else
:
if
shell
:
IO
.
write
(
"-> (
%s
)
%s
"
%
(
p
.
pid
,
cmd
))
else
:
IO
.
write
(
"-> (
%s
)
%s
"
%
(
p
.
pid
,
" "
.
join
(
cmd
)))
output
=
p
.
communicate
(
s_input
)[
0
]
if
output
!=
""
:
IO
.
write
(
"| (
%s
) "
%
p
.
pid
+
re
.
sub
(
r"\n"
,
"
\n
| (
%s
) "
%
p
.
pid
,
output
))
if
p
.
returncode
==
0
:
IO
.
write
(
"ok (
%s
)"
%
p
.
pid
)
else
:
IO
.
write
(
"Error: Returned non-zero exit status
%d
(
%s
)"
%
(
p
.
returncode
,
p
.
pid
))
return
p
.
returncode
==
0
def
read_options
():
"""
Parse command line args
"""
print
" "
.
join
(
sys
.
argv
)
parser
=
argparse
.
ArgumentParser
(
description
=
"EPFL Roaming."
)
parser
.
add_argument
(
"--pam"
,
help
=
"PAM related actions (!=lightdm) : filers (u)mount, folders/files link/copy, GConf/DConf save"
,
action
=
"store_const"
,
dest
=
"context"
,
default
=
None
,
const
=
"pam"
,
)
parser
.
add_argument
(
"--session"
,
help
=
"Session (GConf/DConf load)"
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"session"
,
)
parser
.
add_argument
(
"--on_halt"
,
help
=
"Hold it's execution until all session have been cleaned by epfl_roaming.py (for shutdown/reboot)."
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"on_halt"
,
)
parser
.
add_argument
(
"--test_load"
,
help
=
"Load GConf and DConf (test)"
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"test_load"
,
)
parser
.
add_argument
(
"--test_dump"
,
help
=
"Dump GConf and DConf (test)"
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"test_dump"
,
)
parser
.
add_argument
(
"--torque_prologue"
,
help
=
"Prepare home dir for Torque job"
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"torque_prologue"
,
)
parser
.
add_argument
(
"--torque_epilogue"
,
help
=
"Close home dir after Torque job"
,
action
=
"store_const"
,
dest
=
"context"
,
const
=
"torque_epilogue"
,
)
options
=
parser
.
parse_args
()
if
options
.
context
==
None
:
parser
.
print_help
()
sys
.
exit
(
1
)
return
options
def
read_user
(
options
,
on_halt_username
=
None
):
"""
Extract all necessary info for the user
"""
user
=
NameSpace
()
if
options
.
context
==
"pam"
:
user
.
username
=
os
.
environ
.
get
(
"PAM_USER"
,
None
)
# SERVICE : lightdm | sshd | login
# other services (like slurm) are not taken in account in epfl_roaming
user
.
conn_service
=
os
.
environ
.
get
(
"PAM_SERVICE"
,
None
)
# TTY : :0 | ssh
user
.
conn_tty
=
os
.
environ
.
get
(
"PAM_TTY"
,
None
)
# TYPE : open_session | close_session
user
.
conn_type
=
os
.
environ
.
get
(
"PAM_TYPE"
,
None
)
elif
options
.
context
in
(
"torque_prologue"
,
"torque_epilogue"
):
user
.
username
=
os
.
environ
.
get
(
"USER"
,
None
)
elif
options
.
context
==
"on_halt"
:
user
.
username
=
on_halt_username
else
:
user
.
username
=
pwd
.
getpwuid
(
os
.
getuid
())[
0
]
user
.
home_dir
=
os
.
path
.
expanduser
(
"~
%s
"
%
user
.
username
)
# shortcuts
if
(
options
.
context
in
(
"torque_prologue"
,
"torque_epilogue"
))
or
\
(
not
user
.
home_dir
.
startswith
(
"/home/"
)):
return
user
try
:
pw
=
pwd
.
getpwnam
(
user
.
username
)
except
(
KeyError
,
TypeError
):
return
user
#~ if options.context in ("pam", "on_halt"):
my_ldap
=
Ldap
()
try
:
ldap_res
=
my_ldap
.
search_s
(
l_filter
=
"uidNumber=
%s
"
%
pw
.
pw_uid
,
l_attrs
=
[
"uniqueIdentifier"
]
)
unique_identifier
=
ldap_res
[
0
][
1
][
"uniqueIdentifier"
][
0
]
except
(
KeyError
,
IndexError
):
# no pw_uid or not in ldap!
return
user
# EPFL Guests
if
ldap_res
[
0
][
0
]
.
endswith
(
"o=epfl-guests,c=ch"
):
user
.
epfl_account_type
=
"guest"
return
user
# Normal EPFL account
automount_informations
=
(
""
,
""
,
""
,
""
)
ldap_res
=
my_ldap
.
search_s
(
l_filter
=
"cn=
%s
"
%
user
.
username
,
l_attrs
=
[
"automountInformation"
]
)
for
entry
in
ldap_res
:
if
entry
[
1
]
.
get
(
"automountInformation"
)
!=
None
:
automount_informations
=
entry
[
1
][
"automountInformation"
][
0
]
automount_informations
=
re
.
findall
(
r'-fstype=(\w+),(.+) ([\w\.]+):(.+)$'
,
automount_informations
)[
0
]
gr
=
grp
.
getgrgid
(
pw
.
pw_gid
)
user
.
epfl_account_type
=
"normal"
user
.
uid
=
str
(
pw
.
pw_uid
)
user
.
gid
=
str
(
pw
.
pw_gid
)
user
.
groupname
=
gr
.
gr_name
user
.
domain
=
"INTRANET"
user
.
sciper
=
unique_identifier
user
.
sciper_digit
=
unique_identifier
[
-
1
]
user
.
automount_fstype
=
automount_informations
[
0
]
user
.
automount_options
=
automount_informations
[
1
]
user
.
automount_host
=
automount_informations
[
2
]
user
.
automount_path
=
automount_informations
[
3
]
return
user
def
check_options
(
options
,
user
):
"""
Performs all required checks
"""
#~ if options.context == "pam" and user.conn_service == "lightdm":
#~ IO.write("Not doing things for lightdm sessions trigged by PAM")
#~ sys.exit(0)
if
options
.
context
==
"pam"
and
user
.
conn_service
not
in
(
"lightdm"
,
"sshd"
,
"login"
):
IO
.
write
(
"Not doing anything for PAM_SERVICE '
%s
'"
%
user
.
conn_service
)
sys
.
exit
(
0
)
if
options
.
context
in
(
"pam"
,
"on_halt"
,
"torque_prologue"
,
"torque_epilogue"
)
and
os
.
geteuid
()
!=
0
:
IO
.
write
(
"Error: this should be run as root."
)
sys
.
exit
(
1
)
if
options
.
context
==
"session"
and
os
.
geteuid
()
==
0
:
IO
.
write
(
"Error: this should not be running as root."
)
sys
.
exit
(
1
)
if
user
.
username
==
None
:
if
options
.
context
==
"pam"
:
IO
.
write
(
"Error: Could not read PAM_USER"
)
else
:
IO
.
write
(
"Error: Could not read USER"
)
sys
.
exit
(
1
)
if
not
user
.
home_dir
.
startswith
(
"/home/"
):
IO
.
write
(
"Nothing to do for user
%s
(home dir:
%s
)"
%
(
user
.
username
,
user
.
home_dir
))
sys
.
exit
(
0
)
if
options
.
context
==
"pam"
:
if
user
.
conn_type
==
None
:
IO
.
write
(
"Error: Could not read PAM_TYPE"
)
sys
.
exit
(
1
)
if
user
.
conn_type
not
in
(
"open_session"
,
"close_session"
):
IO
.
write
(
"Error: Unknown PAM_TYPE :
%s
"
%
user
.
conn_type
)
sys
.
exit
(
1
)
if
options
.
context
in
(
"pam"
,
"on_halt"
):
try
:
user
.
epfl_account_type
except
AttributeError
:
IO
.
write
(
"Warning: Incomplete user informations found. Exiting."
)
sys
.
exit
(
1
)
def
apply_subst
(
name
,
user
):
name
=
re
.
sub
(
r'_SCIPER_DIGIT_'
,
user
.
sciper_digit
,
name
)
name
=
re
.
sub
(
r'_SCIPER_'
,
user
.
sciper
,
name
)
name
=
re
.
sub
(
r'_USERNAME_'
,
user
.
username
,
name
)
name
=
re
.
sub
(
r'_GROUPNAME_'
,
user
.
groupname
,
name
)
name
=
re
.
sub
(
r'_DOMAIN_'
,
user
.
domain
,
name
)
name
=
re
.
sub
(
r'_UID_'
,
user
.
uid
,
name
)
name
=
re
.
sub
(
r'_GID_'
,
user
.
gid
,
name
)
name
=
re
.
sub
(
r'_FSTYPE_'
,
user
.
automount_fstype
,
name
)
name
=
re
.
sub
(
r'_HOST_'
,
user
.
automount_host
,
name
)
name
=
re
.
sub
(
r'_PATH_'
,
user
.
automount_path
,
name
)
name
=
re
.
sub
(
r'_OPTIONS_'
,
user
.
automount_options
,
name
)
return
name
def
read_config
(
options
,
user
):
"""
Read and Parse config file
"""
class
ConfigLineException
(
Exception
):
def
__init__
(
self
,
line
,
reason
=
"syntax"
):
self
.
line
=
line
self
.
reason
=
reason
conf
=
{
"mounts"
:
{},
"links"
:
[],
"su_links"
:
[],
"gconf"
:
{},
"dconf"
:
{},}
if
options
.
context
in
(
"torque_prologue"
,
"torque_epilogue"
):
return
conf
gconf_file
=
""
dconf_file
=
""
try
:
with
open
(
CONFIG_FILE
,
"r"
)
as
f
:
for
line
in
f
:
try
:
line
=
re
.
sub
(
r'\s*#.*$'
,
''
,
line
)
.
rstrip
()
if
line
==
""
:
continue
try
:
subject
=
re
.
findall
(
r'(\S+)'
,
line
)[
0
]
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## Mounts
if
subject
==
"mount"
:
if
not
options
.
context
in
(
"pam"
,
"on_halt"
):
continue
line
=
apply_subst
(
line
,
user
)
mount_point
=
get_mount_point
(
line
)
conf
[
"mounts"
][
mount_point
]
=
line
## Links
elif
subject
==
"link"
:
try
:
target
,
link_name
=
re
.
findall
(
r'"([^"]+)"'
,
line
)[
0
:
2
]
target
=
apply_subst
(
target
,
user
)
link_name
=
apply_subst
(
link_name
,
user
)
conf
[
"links"
]
.
append
((
target
,
link_name
))
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## Links
elif
subject
==
"su_link"
:
try
:
target
,
link_name
=
re
.
findall
(
r'"([^"]+)"'
,
line
)[
0
:
2
]
target
=
apply_subst
(
target
,
user
)
link_name
=
apply_subst
(
link_name
,
user
)
conf
[
"su_links"
]
.
append
((
target
,
link_name
))
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## gconf file
elif
subject
==
"gconf_file"
:
try
:
gconf_file
=
re
.
findall
(
r'"(.+)"'
,
line
)[
0
]
gconf_file
=
apply_subst
(
gconf_file
,
user
)
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## gconf entry
elif
subject
==
"gconf"
:
if
gconf_file
==
""
:
raise
ConfigLineException
(
line
,
reason
=
"gconf key before gconf_file instruction"
)
try
:
gconf_entry
=
re
.
findall
(
r'"(.+)"'
,
line
)[
0
]
conf
[
"gconf"
]
.
setdefault
(
gconf_file
,
[])
.
append
(
gconf_entry
)
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## dconf file
elif
subject
==
"dconf_file"
:
try
:
dconf_file
=
re
.
findall
(
r'"(.+)"'
,
line
)[
0
]
dconf_file
=
apply_subst
(
dconf_file
,
user
)
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
## dconf entry
elif
subject
==
"dconf"
:
if
dconf_file
==
""
:
raise
ConfigLineException
(
line
,
reason
=
"dconf key before dconf_file instruction"
)
try
:
dconf_entry
=
re
.
findall
(
r'"(.+)"'
,
line
)[
0
]
conf
[
"dconf"
]
.
setdefault
(
dconf_file
,
[])
.
append
(
dconf_entry
)
except
IndexError
,
e
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
else
:
raise
ConfigLineException
(
line
,
reason
=
"syntax"
)
except
ConfigLineException
,
e
:
IO
.
write
(
"Error: "
,
eol
=
""
)
if
e
.
reason
==
"syntax"
:
IO
.
write
(
"Unrecognized line :
\n
%s
"
%
e
.
line
)
else
:
IO
.
write
(
"
%s
:
\n
%s
"
%
(
e
.
reason
,
e
.
line
))
IO
.
write
(
"Continuing ignoring that one."
)
except
IOError
:
IO
.
write
(
"Conf file
%s
not readable"
%
CONFIG_FILE
)
return
conf
###
# Clean DConf (remove englobing elements)
for
dconf_file
in
conf
[
"dconf"
]:
indexes_to_drop
=
set
()
for
i
in
xrange
(
len
(
conf
[
"dconf"
][
dconf_file
])):
for
j
in
xrange
(
len
(
conf
[
"dconf"
][
dconf_file
])):
if
i
==
j
:
continue
if
conf
[
"dconf"
][
dconf_file
][
i
]
.
startswith
(
conf
[
"dconf"
][
dconf_file
][
j
]):
indexes_to_drop
.
add
(
i
)
for
i
in
reversed
(
sorted
(
list
(
indexes_to_drop
))):
del
(
conf
[
"dconf"
][
dconf_file
][
i
])
return
conf
def
count_sessions
(
user
,
increment
):
"""
Increments/decrements session count for current user
"""
user_count_file
=
"/tmp/epfl_count_
%s
"
%
user
.
username
IO
.
write
(
"session counter :"
,
eol
=
""
)
counter
=
0
# default
try
:
with
open
(
user_count_file
,
"r"
)
as
f
:
counter
=
int
(
f
.
readline
())
except
(
IOError
,
ValueError
):
pass
IO
.
write
(
"
%i
->
%i
"
%
(
counter
,
counter
+
increment
))
counter
+=
increment
if
counter
>
0
:
with
open
(
user_count_file
,
"w"
)
as
f
:
f
.
write
(
"
%i
\n
"
%
counter
)
else
:
try
:
os
.
unlink
(
user_count_file
)
except
OSError
:
pass
return
counter
def
get_mount_point
(
mount_instruction
):
"""
Guess mointpoint from a mount instruction
"""
line
=
mount_instruction
line
=
re
.
sub
(
r'-o \S+\s*'
,
''
,
line
)
line
=
re
.
sub
(
r'-t \S+\s*'
,
''
,
line
)
line
=
re
.
sub
(
r'-[fnrsvw]\s*'
,
''
,
line
)
m
=
re
.
search
(
'(\S+)\s*$'
,
line
)
if
m
:
return
m
.
group
(
1
)
else
:
IO
.
write
(
"Error: Mount point not found in
%s
"
%
mount_instruction
)
IO
.
write
(
"Aborting"
)
sys
.
exit
(
1
)
def
get_credentials
(
username
):
cred_filename
=
"/tmp/
%s
_epfl_cred"
%
username
# Decode credential
def
decode
(
username
,
enc_password
):
username
=
unicode
(
username
,
'utf-8'
)
factor
=
len
(
enc_password
)
/
len
(
username
)
+
1
key
=
username
*
factor
password
=
""
.
join
([
unichr
(
ord
(
enc_password
[
i
])
-
ord
(
key
[
i
]))
for
i
in
range
(
0
,
len
(
enc_password
))
])
return
password
.
encode
(
'utf-8'
)
try
:
with
open
(
cred_filename
,
"rb"
)
as
f
:
if
UNLINK_CRED_FILE
:
os
.
unlink
(
cred_filename
)
enc_password
=
pickle
.
load
(
f
)
except
Exception
:
IO
.
write
(
"Warning: could not load file
%s
, skipping."
%
cred_filename
)
return
None
return
decode
(
username
,
enc_password
)
def
ismount
(
path
):
"""
Replaces os.path.ismount which doesn't work for nfsv4 run from root
"""
p
=
subprocess
.
Popen
([
"mount"
],
stdout
=
subprocess
.
PIPE
)
output
=
p
.
communicate
()[
0
]
return
path
in
re
.
findall
(
r' on (\S+)\s+'
,
output
)
def
gconf_dump
(
config
,
user
,
test
=
False
):
return
# NOTE : We had no success with GConf and 12.04 ...
IO
.
write
(
"gconf_dump"
)
try
:
gconf_work_dir
=
os
.
path
.
join
(
user
.
home_dir
,
".gconf"
)
for
gconf_file
in
config
[
"gconf"
]:
gconf_dirs
=
{}
# extract expected dirs (if keys, then add key entry in a list)
for
i
in
xrange
(
len
(
config
[
"gconf"
][
gconf_file
])):
# check that there are no englobing other
skip_this
=
False
path
=
config
[
"gconf"
][
gconf_file
][
i
]
for
j
in
xrange
(
len
(
config
[
"gconf"
][
gconf_file
])):
if
i
==
j
:
continue
if
path
.
startswith
(
config
[
"gconf"
][
gconf_file
][
j
]):
skip_this
=
True
break
if
skip_this
:
continue
# Store dir
if
path
[
-
1
]
==
"/"
:
gconf_dirs
[
path
]
=
[]
else
:
dirname
=
os
.
path
.
dirname
(
path
)
if
dirname
[
-
1
]
!=
"/"
:
dirname
+=
"/"
keyname
=
os
.
path
.
basename
(
path
)
if
len
(
gconf_dirs
.
get
(
dirname
,
[
1
]))
==
0
:
continue
# already included as a dir
gconf_dirs
.
setdefault
(
dirname
,
[])
.
append
(
keyname
)
#~ IO.write("gconf_dirs :")
#~ pprint.pprint(gconf_dirs)
if
len
(
gconf_dirs
)
!=
0
:
# Dump gconf dirs
# "dbus-launch", "--exit-with-session",
# "sudo", "-u", user.username,
# "--config-source=xml:readwrite:%s" % (gconf_work_dir),
if
test
:
cmd
=
[
"gconftool-2"
,
"--config-source=xml:readwrite:
%s
"
%
(
gconf_work_dir
),
"--dump"
]
else
:
cmd
=
[
"sudo"
,
"-u"
,
user
.
username
,
"dbus-launch"
,
"--exit-with-session"
,
"gconftool-2"
,
"--dump"
]
for
gconf_dir
in
gconf_dirs
:
if
gconf_dir
==
"/"
:
cmd
+=
(
gconf_dir
,)
else
:
cmd
+=
(
gconf_dir
[:
-
1
],)
IO
.
write
(
" "
.
join
(
cmd
))
if
test
:
p
=
subprocess
.
Popen
(
cmd
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
)
else
:
p
=
subprocess
.
Popen
(
cmd
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
env
=
{})
complete_dump
,
stderr
=
p
.
communicate
()
if
stderr
!=
""
:
IO
.
write
(
"Error :
\n
"
+
"
\n
"
.
join
([
"EE
%s
"
%
line
for
line
in
stderr
.
split
(
"
\n
"
)]))
# Filter with xml.dom.minidom
dom
=
xml
.
dom
.
minidom
.
parseString
(
complete_dump
)
saved_keys
=
[]
for
entrylist
in
dom
.
getElementsByTagName
(
"entrylist"
):
base
=
entrylist
.
getAttribute
(
"base"
)
for
entry
in
entrylist
.
getElementsByTagName
(
"entry"
):
key_name
=
entry
.
getElementsByTagName
(
"key"
)[
0
]
.
childNodes
[
0
]
.
toxml
()
key_path
=
os
.
path
.
join
(
base
,
key_name
)
# Drop entry if not expected
drop_entry
=
True
for
gconf_dir
in
gconf_dirs
:
if
key_path
.
startswith
(
gconf_dir
):
if
gconf_dirs
[
gconf_dir
]
==
[]:
drop_entry
=
False
else
:
for
key
in
gconf_dirs
[
gconf_dir
]:
if
key_path
==
os
.
path
.
join
(
gconf_dir
,
key
):
drop_entry
=
False
continue
if
drop_entry
:
entrylist
.
removeChild
(
entry
)
else
:
saved_keys
.
append
(
key_path
)
# Save
gconf_file
=
os
.
path
.
join
(
user
.
home_dir
,
gconf_file
)
IO
.
write
(
"Saving to
%s
keys:
\n
%s
"
%
(
gconf_file
,
"
\n
"
.
join
(
saved_keys
)))
dir_save_to
=
os
.
path
.
dirname
(
gconf_file
)
if
not
os
.
path
.
exists
(
dir_save_to
):
IO
.
write
(
"mkdir -p
%s
"
%
dir_save_to
)
os
.
makedirs
(
dir_save_to
)
f
=
open
(
gconf_file
,
"w"
)
#~ dom.writexml(f)
f
.
write
(
dom
.
toxml
(
encoding
=
"utf-8"
))
f
.
close
()
else
:
if
os
.
path
.
exists
(
gconf_file
)
and
os
.
path
.
isfile
(
gconf_file
):
os
.
unlink
(
gconf_file
)
except
Exception
,
e
:
IO
.
write
(
"Unexpected exception :
%s
"
%
e
)
def
gconf_load
(
config
,
user
,
test
=
False
):
return
# NOTE : We had no success with GConf and 12.04 ...
IO
.
write
(
"gconf_load"
)
user_gconf_dir
=
os
.
path
.
join
(
user
.
home_dir
,
".gconf"
)
for
gconf_file
in
config
[
"gconf"
]:
gconf_file
=
os
.
path
.
join
(
user
.
home_dir
,
gconf_file
)
if
os
.
path
.
exists
(
gconf_file
):
# "--direct", "--config-source=xml:readwrite:%s" % user_gconf_dir ,
run_cmd
(
cmd
=
[
"gconftool-2"
,
"--load"
,
gconf_file
],
)
def
dconf_dump
(
config
,
user
,
test
=
False
):
if
not
os
.
path
.
exists
(
os
.
path
.
join
(
user
.
home_dir
,
".config/dconf/user"
)):
IO
.
write
(
"dconf_dump : ~/.config/dconf/user not found -> Skipping."
)
return
IO
.
write
(
"dconf_dump"
)
for
dconf_file
,
keys_to_save
in
config
[
"dconf"
]
.
items
():
dconf_file
=
os
.
path
.
join
(
user
.
home_dir
,
dconf_file
)
IO
.
write
(
"DConf to
%s
"
%
dconf_file
)
dir_save_to
=
os
.
path
.
dirname
(
dconf_file
)
if
not
os
.
path
.
exists
(
dir_save_to
):
IO
.
write
(
"mkdir -p
%s
"
%
dir_save_to
)
os
.
makedirs
(
dir_save_to
)
dump_succeeded
=
True
dump_dconf
=
""
for
k
in
keys_to_save
:
IO
.
write
(
"+
%s
"
%
k
)
if
k
[
-
1
]
==
"/"
:
#
if
test
:
cmd
=
[
"dconf"
,
"dump"
,
k
]
else
:
cmd
=
[
"sudo"
,
"-u"
,
user
.
username
,
"dconf"
,
"dump"
,
k
]
p
=
subprocess
.
Popen
(
cmd
,
stdout
=
subprocess
.
PIPE
,
env
=
{})
k_dumped
=
p
.
communicate
()[
0
]
for
line
in
k_dumped
.
split
(
"
\n
"
):
try
:
fold
=
re
.
findall
(
r'^\[(.*)\]$'
,
line
)[
0
]
if
fold
==
"/"
:
dump_dconf
+=
"[
%s
]
\n
"
%
k
[
1
:
-
1
]
else
:
dump_dconf
+=
"[
%s
]
\n
"
%
os
.
path
.
join
(
k
[
1
:
-
1
],
fold
)
except
IndexError
,
e
:
dump_dconf
+=
line
+
"
\n
"
else
:
#
if
test
:
cmd
=
[
"dconf"
,
"read"
,
k
]
else
:
cmd
=
[
"sudo"
,
"-u"
,
user
.
username
,
"dconf"
,
"read"
,
k
]
p
=
subprocess
.
Popen
(
cmd
,
stdout
=
subprocess
.
PIPE
)
k_dumped
=
p
.
communicate
()[
0
]
if
k_dumped
!=
""
:
dump_dconf
+=
"""
[%s]
%s=%s
"""
%
(
os
.
path
.
dirname
(
k
)[
1
:],
os
.
path
.
basename
(
k
),
k_dumped
)
if
p
.
returncode
!=
0
:
dump_succeeded
=
False
break
if
dump_succeeded
and
dump_dconf
!=
""
:
with
open
(
dconf_file
,
"w"
)
as
f
:
f
.
write
(
dump_dconf
)
else
:
IO
.
write
(
"DConf dump did not succeeded. Aborting this."
)
def
dconf_load
(
config
,
user
,
test
=
False
):
IO
.
write
(
"dconf_load"
)
for
dconf_file
in
config
[
"dconf"
]:
dconf_file
=
os
.
path
.
join
(
user
.
home_dir
,
dconf_file
)
if
os
.
path
.
exists
(
dconf_file
):
with
open
(
dconf_file
,
"r"
)
as
f
:
dconf_dumped
=
f
.
read
()
# "dbus-launch", "--exit-with-session",
cmd
=
[
"dconf"
,
"load"
,
"/"
]
run_cmd
(
cmd
=
cmd
,
s_cmd
=
"cat
%s
|
%s
"
%
(
dconf_file
,
" "
.
join
(
cmd
)),
stdin
=
subprocess
.
PIPE
,
s_input
=
dconf_dumped
,
)
def
filers_mount
(
config
,
user
):
"""
Performs all mount
return True if all succeed
return False if one failed
"""
IO
.
write
(
"Proceeding mount!"
)
success
=
True
if
PASSWORD
!=
None
:
os
.
environ
[
'PASSWD'
]
=
PASSWORD
# For CIFS mounts
for
mount_point
,
mount_instruction
in
config
[
"mounts"
]
.
items
():
if
not
os
.
path
.
exists
(
mount_point
):
#~ os.makedirs(mount_point)
run_cmd
(
cmd
=
[
"mkdir"
,
"-p"
,
mount_point
]
)
run_cmd
(
cmd
=
[
"chown"
,
"
%s
:"
%
user
.
username
,
mount_point
]
)
# chown parents also
parent_dir
=
os
.
path
.
dirname
(
mount_point
)
while
parent_dir
.
startswith
(
"/home/
%s
"
%
user
.
username
):
run_cmd
(
cmd
=
[
"chown"
,
"
%s
:"
%
user
.
username
,
parent_dir
]
)
parent_dir
=
os
.
path
.
dirname
(
parent_dir
)
run_cmd
(
cmd
=
mount_instruction
,
shell
=
True
,
)
if
not
ismount
(
mount_point
):
success
=
False
if
PASSWORD
!=
None
:
del
os
.
environ
[
'PASSWD'
]
return
success
def
filers_umount
(
config
,
user
):
"""
Performs all umount
return True if all succeed
return False if one failed
"""
IO
.
write
(
"Proceeding umount!"
)
success
=
True
for
mount_point
in
config
[
"mounts"
]
.
keys
()
+
[
os
.
path
.
join
(
user
.
home_dir
,
".gvfs"
),]:
if
not
ismount
(
mount_point
):
IO
.
write
(
"
%s
not mounted. Skip."
%
mount_point
)
continue
for
i
in
xrange
(
UMOUNT_MAX_ATTEMPT
):
if
run_cmd
(
cmd
=
[
"umount"
,
"-fl"
,
mount_point
],
):
break
time
.
sleep
(
UMOUNT_SLEEP
)
if
ismount
(
mount_point
):
success
=
False
return
success
def
make_homedir
(
user
):
## Make homedir
if
not
os
.
path
.
exists
(
user
.
home_dir
):
IO
.
write
(
"Make homedir"
)
run_cmd
(
cmd
=
[
"cp"
,
"-R"
,
"/etc/skel"
,
user
.
home_dir
]
)
run_cmd
(
cmd
=
[
"chown"
,
"-R"
,
"
%s
:"
%
user
.
username
,
user
.
home_dir
]
)
else
:
IO
.
write
(
"homedir already exists."
)
def
proceed_roaming_open
(
config
,
user
):
IO
.
write
(
"Proceeding roaming 'open'!"
)
## Make homedir
make_homedir
(
user
)
## Mounts (sudo)
filers_mount
(
config
,
user
)
## Links
with
UserIdentity
(
user
):
for
target
,
link_name
in
config
[
"links"
]:
target_is_dir
=
False
force_target
=
False
if
re
.
search
(
r'/$'
,
target
):
target_is_dir
=
True
if
re
.
match
(
r'\+'
,
target
):
force_target
=
True
target
=
target
[
1
:]
target
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
target
))
target_parent
=
os
.
path
.
normpath
(
target
+
"/.."
)
link_name
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
link_name
))
link_name_parent
=
os
.
path
.
normpath
(
link_name
+
"/.."
)
if
not
os
.
path
.
lexists
(
target
):
if
force_target
:
if
target_is_dir
:
os
.
makedirs
(
target
)
# mkdir target
else
:
if
not
os
.
path
.
lexists
(
target_parent
):
os
.
makedirs
(
target_parent
)
open
(
target
,
"a"
)
.
close
()
# touch target
else
:
continue
if
os
.
path
.
lexists
(
link_name
):
if
os
.
path
.
exists
(
target
):
# link_name and target exists -> use target
if
os
.
path
.
isdir
(
link_name
)
and
not
os
.
path
.
islink
(
link_name
):
shutil
.
rmtree
(
link_name
)
else
:
os
.
unlink
(
link_name
)
else
:
continue
if
not
os
.
path
.
exists
(
link_name_parent
):
os
.
makedirs
(
link_name_parent
)
IO
.
write
(
"ln -s
%s
%s
"
%
(
target
,
link_name
))
os
.
symlink
(
target
,
link_name
)
## su_Links (links done as root and chown afterward)
for
target
,
link_name
in
config
[
"su_links"
]:
target_is_dir
=
False
force_target
=
False
if
re
.
search
(
r'/$'
,
target
):
target_is_dir
=
True
if
re
.
match
(
r'\+'
,
target
):
force_target
=
True
target
=
target
[
1
:]
target
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
target
))
target_parent
=
os
.
path
.
normpath
(
target
+
"/.."
)
link_name
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
link_name
))
link_name_parent
=
os
.
path
.
normpath
(
link_name
+
"/.."
)
if
not
os
.
path
.
lexists
(
target
):
if
force_target
:
if
target_is_dir
:
os
.
makedirs
(
target
)
# mkdir target
else
:
if
not
os
.
path
.
lexists
(
target_parent
):
os
.
makedirs
(
target_parent
)
open
(
target
,
"a"
)
.
close
()
# touch target
run_cmd
(
cmd
=
[
"chown"
,
"
%s
:"
%
user
.
username
,
target
]
)
else
:
continue
if
os
.
path
.
lexists
(
link_name
):
if
os
.
path
.
exists
(
target
):
# link_name and target exists -> use target
if
os
.
path
.
isdir
(
link_name
)
and
not
os
.
path
.
islink
(
link_name
):
shutil
.
rmtree
(
link_name
)
else
:
os
.
unlink
(
link_name
)
else
:
continue
if
not
os
.
path
.
exists
(
link_name_parent
):
os
.
makedirs
(
link_name_parent
)
IO
.
write
(
"ln -s
%s
%s
"
%
(
target
,
link_name
))
os
.
symlink
(
target
,
link_name
)
run_cmd
(
cmd
=
[
"chown"
,
"-h"
,
"
%s
:"
%
user
.
username
,
link_name
]
)
def
proceed_roaming_close
(
options
,
config
,
user
):
IO
.
write
(
"Proceeding roaming 'close'!"
)
## Links
for
target
,
link_name
in
config
[
"links"
]:
if
re
.
match
(
r'\+'
,
target
):
target
=
target
[
1
:]
target
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
target
))
target_parent
=
os
.
path
.
normpath
(
target
+
"/.."
)
link_name
=
os
.
path
.
normpath
(
os
.
path
.
join
(
user
.
home_dir
,
link_name
))
link_name_parent
=
os
.
path
.
normpath
(
link_name
+
"/.."
)
if
os
.
path
.
exists
(
link_name
):
if
os
.
path
.
realpath
(
link_name
)
!=
os
.
path
.
realpath
(
target
):
# link_name doesn't point to target -> new content -> rm old content.
run_cmd
(
cmd
=
[
"rm"
,
"-rf"
,
"--one-file-system"
,
target
],
)
if
not
os
.
path
.
exists
(
target
):
if
not
os
.
path
.
lexists
(
target_parent
):
IO
.
write
(
"mkdir -p
%s
"
%
target_parent
)
os
.
makedirs
(
target_parent
)
if
os
.
path
.
isdir
(
link_name
):
run_cmd
(
cmd
=
[
"cp"
,
"-R"
,
link_name
,
target
],
)
else
:
run_cmd
(
cmd
=
[
"cp"
,
link_name
,
target
],
)
#~ run_cmd(
#~ cmd=["sync"]
#~ )
gconf_dump
(
config
,
user
)
dconf_dump
(
config
,
user
)
## Umounts (sudo)
if
not
filers_umount
(
config
,
user
):
IO
.
write
(
"Skipping rm -rf."
)
return
## RM
for
i
in
xrange
(
RM_MAX_ATTEMPT
):
success
=
run_cmd
(
cmd
=
[
"rm"
,
"-rf"
,
"--one-file-system"
,
user
.
home_dir
]
)
if
success
:
break
time
.
sleep
(
RM_SLEEP
)
def
proceed_guest_open
(
user
):
IO
.
write
(
"Proceeding guest 'open'!"
)
make_homedir
(
user
)
def
proceed_guest_close
(
user
):
IO
.
write
(
"Proceeding guest 'close'!"
)
IO
.
write
(
"Nothing done ..."
)
def
proceed_torque_prologue
(
config
,
user
):
IO
.
write
(
"Proceeding Torque Prolog!"
)
## Make Home dir
if
not
os
.
path
.
exists
(
user
.
home_dir
):
IO
.
write
(
"Make homedir"
)
run_cmd
(
cmd
=
[
"cp"
,
"-R"
,
"/etc/skel"
,
user
.
home_dir
]
)
run_cmd
(
cmd
=
[
"chown"
,
"-R"
,
"
%s
:"
%
user
.
username
,
user
.
home_dir
]
)
else
:
IO
.
write
(
"Home dir already exists: nothing to do."
)
def
proceed_torque_epilogue
(
config
,
user
):
IO
.
write
(
"Proceeding Torque Epilog!"
)
## Remove Home dir
if
os
.
path
.
exists
(
user
.
home_dir
):
for
i
in
xrange
(
RM_MAX_ATTEMPT
):
success
=
run_cmd
(
cmd
=
[
"rm"
,
"-rf"
,
"--one-file-system"
,
user
.
home_dir
]
)
if
success
:
break
time
.
sleep
(
RM_SLEEP
)
else
:
IO
.
write
(
"Home dir doesn't exists: nothing to do."
)
def
proceed_on_halt
(
options
):
def
list_current_users
():
return
[
f
[
11
:]
for
f
in
os
.
listdir
(
"/tmp"
)
if
f
.
startswith
(
"epfl_count"
)]
with
IO
(
LOG_PAM
):
IO
.
write
(
"
\n
***
%s
"
%
datetime
.
datetime
.
now
())
IO
.
write
(
"Proceeding 'On Halt'!"
)
try
:
with
PreventInterrupt
():
with
lockfile
.
FileLock
(
SEMAPHORE_LOCK_FILE
):
for
username
in
list_current_users
():
IO
.
write
(
"on_halt
%s
"
%
username
)
user
=
read_user
(
options
,
username
)
config
=
read_config
(
options
,
user
)
proceed_roaming_close
(
options
,
config
,
user
)
except
Exception
,
e
:
exc_type
,
exc_value
,
exc_traceback
=
sys
.
exc_info
()
IO
.
write
(
"
\n
"
.
join
(
traceback
.
format_exception
(
exc_type
,
exc_value
,
exc_traceback
)))
IO
.
write
(
"done."
)
def
signal_handler
(
signum
,
frame
):
IO
.
write
(
"received signal
%s
"
%
signum
)
if
PreventInterrupt
.
is_interruptible
():
IO
.
write
(
"exit."
)
sys
.
exit
(
1
)
else
:
IO
.
write
(
"not interruptible yet. Continuing..."
)
### MAIN
if
__name__
==
'__main__'
:
username
=
os
.
environ
.
get
(
"PAM_USER"
,
None
)
if
username
is
not
None
:
PASSWORD
=
get_credentials
(
username
)
# Manage the kill -TERM ... unfortunately not kill -9
signal
.
signal
(
signal
.
SIGTERM
,
signal_handler
)
#~ signal.signal(signal.SIGKILL, signal_handler)
options
=
read_options
()
if
options
.
context
==
"on_halt"
:
proceed_on_halt
(
options
)
sys
.
exit
(
0
)
user
=
read_user
(
options
)
if
options
.
context
in
(
"pam"
,
"torque_prologue"
,
"torque_epilogue"
):
logfile_name
=
LOG_PAM
else
:
logfile_name
=
LOG_SESSION
.
format
(
username
=
user
.
username
)
EPFL_ROAMING_DONE_FILE
=
os
.
path
.
join
(
"/tmp/epfl_roaming_{}_done"
.
format
(
user
.
username
))
with
IO
(
logfile_name
):
# IO.write(pprint.pformat(user))
#~ IO.write("ENV :")
#~ IO.write(pprint.pformat(os.environ))
#~ IO.write("\n")
try
:
IO
.
write
(
"
\n
***
%s
"
%
datetime
.
datetime
.
now
())
operation
=
options
.
context
if
options
.
context
==
"pam"
:
operation
+=
"_
%s
"
%
user
.
conn_type
IO
.
write
(
"
%s
%s
(uid=
%s
euid=
%s
)"
%
(
operation
,
user
.
username
,
os
.
getuid
(),
os
.
geteuid
()))
check_options
(
options
,
user
)
# EPFL Guests shortcut
if
user
.
epfl_account_type
==
"guest"
:
if
options
.
context
==
"pam"
:
if
user
.
conn_type
==
"open_session"
:
proceed_guest_open
(
user
)
elif
user
.
conn_type
==
"close_session"
:
proceed_guest_close
(
user
)
sys
.
exit
(
0
)
config
=
read_config
(
options
,
user
)
#~ IO.write("options")
#~ IO.write(pprint.pformat(options))
#~ IO.write("user")
#~ IO.write(pprint.pformat(user))
#~ IO.write("config")
#~ IO.write(pprint.pformat(config))
#~ sys.exit(0)
if
options
.
context
==
"pam"
:
if
user
.
conn_type
==
"open_session"
:
with
lockfile
.
FileLock
(
SEMAPHORE_LOCK_FILE
):
count_sessions
(
user
,
+
1
)
if
PASSWORD
is
not
None
and
not
os
.
path
.
exists
(
EPFL_ROAMING_DONE_FILE
):
proceed_roaming_open
(
config
,
user
)
with
open
(
EPFL_ROAMING_DONE_FILE
,
"w"
):
pass
elif
user
.
conn_type
==
"close_session"
:
time
.
sleep
(
0.5
)
# Give on_halt the chance to be the 1st!
with
lockfile
.
FileLock
(
SEMAPHORE_LOCK_FILE
):
if
count_sessions
(
user
,
-
1
)
==
0
:
with
PreventInterrupt
():
proceed_roaming_close
(
options
,
config
,
user
)
os
.
unlink
(
EPFL_ROAMING_DONE_FILE
)
elif
options
.
context
==
"session"
:
gconf_load
(
config
,
user
)
dconf_load
(
config
,
user
)
elif
options
.
context
==
"torque_prologue"
:
with
lockfile
.
FileLock
(
SEMAPHORE_LOCK_FILE
):
proceed_torque_prologue
(
config
,
user
)
elif
options
.
context
==
"torque_epilogue"
:
with
lockfile
.
FileLock
(
SEMAPHORE_LOCK_FILE
):
if
count_sessions
(
user
,
0
)
==
0
:
proceed_torque_epilogue
(
config
,
user
)
else
:
IO
.
write
(
"Sessions still opened for user
%s
. Nothing to do."
%
user
.
username
)
elif
options
.
context
==
"test_load"
:
gconf_load
(
config
,
user
,
test
=
True
)
dconf_load
(
config
,
user
,
test
=
True
)
elif
options
.
context
==
"test_dump"
:
gconf_dump
(
config
,
user
,
test
=
True
)
dconf_dump
(
config
,
user
,
test
=
True
)
IO
.
write
(
"Everything complete."
)
sys
.
exit
(
0
)
except
Exception
,
e
:
exc_type
,
exc_value
,
exc_traceback
=
sys
.
exc_info
()
IO
.
write
(
"
\n
"
.
join
(
traceback
.
format_exception
(
exc_type
,
exc_value
,
exc_traceback
)))
Event Timeline
Log In to Comment