Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
slapos.package
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Douglas
slapos.package
Commits
b9ac2fef
Commit
b9ac2fef
authored
Feb 22, 2014
by
Rafael Monnerat
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[slapos.package] First working version for debian updates
Still working in progress
parent
18007792
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
153 additions
and
60 deletions
+153
-60
setup.py
setup.py
+15
-5
slapos/package/base_promise.py
slapos/package/base_promise.py
+1
-1
slapos/package/distribution.py
slapos/package/distribution.py
+50
-9
slapos/package/signature.py
slapos/package/signature.py
+32
-5
slapos/package/update.py
slapos/package/update.py
+50
-21
slapos/package/upload_key.py
slapos/package/upload_key.py
+5
-19
No files found.
setup.py
View file @
b9ac2fef
from
setuptools
import
setup
from
setuptools
import
setup
,
find_packages
version
=
'0.0.1.1'
# Still under development
version
=
'0.0.1.3'
name
=
'slapos.package'
name
=
'slapos.package'
long_description
=
open
(
"README.txt"
).
read
()
+
"
\
n
"
+
\
long_description
=
open
(
"README.txt"
).
read
()
+
"
\
n
"
+
\
open
(
"CHANGES.txt"
).
read
()
+
"
\
n
"
open
(
"CHANGES.txt"
).
read
()
+
"
\
n
"
...
@@ -16,7 +17,8 @@ setup(name=name,
...
@@ -16,7 +17,8 @@ setup(name=name,
license
=
'GPLv3'
,
license
=
'GPLv3'
,
url
=
'http://www.slapos.org'
,
url
=
'http://www.slapos.org'
,
author
=
'VIFIB'
,
author
=
'VIFIB'
,
packages
=
[
'slapos.package'
],
namespace_packages
=
[
'slapos'
],
packages
=
find_packages
(),
include_package_data
=
True
,
include_package_data
=
True
,
install_requires
=
[
install_requires
=
[
'slapos.libnetworkcache'
,
'slapos.libnetworkcache'
,
...
@@ -25,8 +27,16 @@ setup(name=name,
...
@@ -25,8 +27,16 @@ setup(name=name,
zip_safe
=
False
,
zip_safe
=
False
,
entry_points
=
{
entry_points
=
{
'console_scripts'
:
[
'console_scripts'
:
[
'slapos-update = slapos.package.update:main'
,
# Those entry points are development version
]
'slappkg-update = slapos.package.update:main'
,
'slappkg-discover = slapos.package.distribution:do_discover'
,
'slappkg-upload-key = slapos.package.upload_key:main'
],
# Not supported yet
#'slapos.cli': [
# 'package upload-key = slapos.package.upload_key:main'
# ]
},
},
test_suite
=
"slapos.package.test"
,
test_suite
=
"slapos.package.test"
,
)
)
slapos/package/base_promise.py
View file @
b9ac2fef
...
@@ -55,7 +55,7 @@ class BasePromise(PackageManager):
...
@@ -55,7 +55,7 @@ class BasePromise(PackageManager):
self
.
log
(
"Calling: %s"
%
' '
.
join
(
cmd_args
))
self
.
log
(
"Calling: %s"
%
' '
.
join
(
cmd_args
))
if
not
dry_run
:
if
not
dry_run
:
p
=
sub
.
Popen
(
cmd_args
,
stdout
=
stdout
,
stderr
=
stderr
)
p
=
sub
process
.
Popen
(
cmd_args
,
stdout
=
stdout
,
stderr
=
stderr
)
output
,
err
=
p
.
communicate
()
output
,
err
=
p
.
communicate
()
return
output
,
err
return
output
,
err
...
...
slapos/package/distribution.py
View file @
b9ac2fef
import
platform
import
platform
import
glob
import
re
import
re
import
os
_distributor_id_file_re
=
re
.
compile
(
"(?:DISTRIB_ID
\
s*=)
\
s*(.*)"
,
re
.
I
)
_distributor_id_file_re
=
re
.
compile
(
"(?:DISTRIB_ID
\
s*=)
\
s*(.*)"
,
re
.
I
)
_release_file_re
=
re
.
compile
(
"(?:DISTRIB_RELEASE
\
s*=)
\
s*(.*)"
,
re
.
I
)
_release_file_re
=
re
.
compile
(
"(?:DISTRIB_RELEASE
\
s*=)
\
s*(.*)"
,
re
.
I
)
...
@@ -30,6 +32,13 @@ def patched_linux_distribution(distname='', version='', id='',
...
@@ -30,6 +32,13 @@ def patched_linux_distribution(distname='', version='', id='',
return
platform
.
linux_distribution
(
distname
,
version
,
id
,
supported_dists
,
full_distribution_name
)
return
platform
.
linux_distribution
(
distname
,
version
,
id
,
supported_dists
,
full_distribution_name
)
class
PackageManager
:
class
PackageManager
:
def
matchSignatureList
(
self
,
signature_list
):
return
self
.
getOSSignature
()
in
signature_list
def
getOSSignature
(
self
):
return
"+++"
.
join
(
patched_linux_distribution
())
def
getDistributionName
(
self
):
def
getDistributionName
(
self
):
return
patched_linux_distribution
()[
0
]
return
patched_linux_distribution
()[
0
]
...
@@ -62,36 +71,60 @@ class PackageManager:
...
@@ -62,36 +71,60 @@ class PackageManager:
""" Add a repository """
""" Add a repository """
return
self
.
_getDistribitionHandler
().
updateRepository
(
self
.
_call
)
return
self
.
_getDistribitionHandler
().
updateRepository
(
self
.
_call
)
def
_installSoftware
(
self
,
name
):
def
_installSoftware
List
(
self
,
name_list
):
""" Upgrade softwares """
""" Upgrade softwares """
return
self
.
_getDistribitionHandler
().
installSoftware
(
self
.
_call
,
name
)
return
self
.
_getDistribitionHandler
().
installSoftware
List
(
self
.
_call
,
name_list
)
def
_updateSoftware
(
self
):
def
_updateSoftware
(
self
):
""" Upgrade softwares """
""" Upgrade softwares """
return
self
.
_getDistribitionHandler
().
updateSoftware
(
self
.
_call
)
return
self
.
_getDistribitionHandler
().
updateSoftware
(
self
.
_call
)
def
updateSystem
(
self
):
def
_
updateSystem
(
self
):
""" Dist-Upgrade of system """
""" Dist-Upgrade of system """
return
self
.
_getDistribitionHandler
().
updateSystem
(
self
.
_call
)
return
self
.
_getDistribitionHandler
().
updateSystem
(
self
.
_call
)
def
update
(
self
,
repository_list
=
[],
package_list
=
[]):
""" Perform upgrade """
self
.
_purgeRepository
()
for
alias
,
url
in
repository_list
:
self
.
_addRepository
(
url
,
alias
)
self
.
_updateRepository
()
if
len
(
package_list
):
self
.
_installSoftwareList
(
package_list
)
# This helper implements API for package handling
# This helper implements API for package handling
class
AptGet
:
class
AptGet
:
source_list_path
=
"/etc/apt/sources.list"
source_list_d_path
=
"/etc/apt/sources.list.d"
def
purgeRepository
(
self
,
caller
):
def
purgeRepository
(
self
,
caller
):
""" Remove all repositories """
""" Remove all repositories """
raise
NotImplemented
# Aggressive removal
os
.
remove
(
self
.
source_list_path
)
open
(
"/etc/apt/sources.list"
,
"w+"
).
write
(
"# Removed all"
)
for
file_path
in
glob
.
glob
(
"%s/*"
%
self
.
source_list_d_path
):
os
.
remove
(
file_path
)
def
addRepository
(
self
,
caller
,
url
,
alias
):
def
addRepository
(
self
,
caller
,
url
,
alias
):
""" Add a repository """
""" Add a repository """
raise
NotImplemented
repos_file
=
open
(
"%s/%s.list"
%
(
self
.
source_list_d_path
,
alias
),
"w"
)
prefix
=
"deb "
if
alias
.
endswith
(
"-src"
):
prefix
=
"deb-src "
repos_file
.
write
(
prefix
+
url
)
repos_file
.
close
()
def
updateRepository
(
self
,
caller
):
def
updateRepository
(
self
,
caller
):
""" Add a repository """
""" Add a repository """
caller
([
'apt-get'
,
'update'
],
stdout
=
None
)
caller
([
'apt-get'
,
'update'
],
stdout
=
None
)
def
installSoftware
(
self
,
caller
,
name
):
def
installSoftware
List
(
self
,
caller
,
name_list
):
""" Instal Software """
""" Instal Software """
self
.
updateRepository
(
caller
)
self
.
updateRepository
(
caller
)
caller
([
"apt-get"
,
"install"
,
"-y"
,
name
],
stdout
=
None
)
command_list
=
[
"apt-get"
,
"install"
,
"-y"
]
command_list
.
extend
(
name_list
)
caller
(
command_list
,
stdout
=
None
)
def
isUpgradable
(
self
,
caller
,
name
):
def
isUpgradable
(
self
,
caller
,
name
):
output
,
err
=
caller
([
"apt-get"
,
"upgrade"
,
"--dry-run"
])
output
,
err
=
caller
([
"apt-get"
,
"upgrade"
,
"--dry-run"
])
...
@@ -132,10 +165,12 @@ class Zypper:
...
@@ -132,10 +165,12 @@ class Zypper:
return
False
return
False
return
True
return
True
def
installSoftware
(
self
,
caller
,
name
):
def
installSoftware
List
(
self
,
caller
,
name_list
):
""" Instal Software """
""" Instal Software """
self
.
updateRepository
(
caller
)
self
.
updateRepository
(
caller
)
caller
([
'zypper'
,
'--gpg-auto-import-keys'
,
'up'
,
'-ly'
,
name
],
stdout
=
None
)
command_list
=
[
'zypper'
,
'--gpg-auto-import-keys'
,
'up'
,
'-ly'
]
command_list
.
extend
(
name_list
)
caller
(
command_list
,
stdout
=
None
)
def
updateSoftware
(
self
,
caller
):
def
updateSoftware
(
self
,
caller
):
""" Upgrade softwares """
""" Upgrade softwares """
...
@@ -145,3 +180,9 @@ class Zypper:
...
@@ -145,3 +180,9 @@ class Zypper:
""" Dist-Upgrade of system """
""" Dist-Upgrade of system """
caller
([
'zypper'
,
'--gpg-auto-import-keys'
,
'dup'
,
'-ly'
],
stdout
=
None
)
caller
([
'zypper'
,
'--gpg-auto-import-keys'
,
'dup'
,
'-ly'
],
stdout
=
None
)
def
do_discover
():
package_manager
=
PackageManager
()
print
package_manager
.
getOSSignature
()
slapos/package/signature.py
View file @
b9ac2fef
...
@@ -73,6 +73,14 @@ class NetworkCache:
...
@@ -73,6 +73,14 @@ class NetworkCache:
else
:
else
:
self
.
directory_key
=
"slapos-upgrade-testing-key"
self
.
directory_key
=
"slapos-upgrade-testing-key"
def
get_yes_no
(
prompt
):
while
True
:
answer
=
raw_input
(
prompt
+
" [y,n]: "
)
if
answer
.
upper
()
in
[
'Y'
,
'YES'
]:
return
True
if
answer
.
upper
()
in
[
'N'
,
'NO'
]:
return
False
class
Signature
:
class
Signature
:
def
__init__
(
self
,
config
,
logger
=
None
):
def
__init__
(
self
,
config
,
logger
=
None
):
...
@@ -92,6 +100,7 @@ class Signature:
...
@@ -92,6 +100,7 @@ class Signature:
for
entry
in
entry_list
:
for
entry
in
entry_list
:
if
entry
[
'timestamp'
]
>
timestamp
:
if
entry
[
'timestamp'
]
>
timestamp
:
best_entry
=
entry
best_entry
=
entry
return
best_entry
return
best_entry
return
helper_download_network_cached_to_file
(
return
helper_download_network_cached_to_file
(
...
@@ -145,7 +154,7 @@ class Signature:
...
@@ -145,7 +154,7 @@ class Signature:
except
Exception
:
except
Exception
:
print
'Unable to upload to cache:
\
n
%s.'
%
traceback
.
format_exc
()
print
'Unable to upload to cache:
\
n
%s.'
%
traceback
.
format_exc
()
def
upload
(
self
,
dry_run
=
0
):
def
upload
(
self
,
dry_run
=
0
,
verbose
=
1
):
upgrade_info
=
ConfigParser
.
RawConfigParser
()
upgrade_info
=
ConfigParser
.
RawConfigParser
()
upgrade_info
.
read
(
self
.
config
.
upgrade_file
)
upgrade_info
.
read
(
self
.
config
.
upgrade_file
)
...
@@ -160,13 +169,17 @@ class Signature:
...
@@ -160,13 +169,17 @@ class Signature:
upgrade_info
.
write
(
file
)
upgrade_info
.
write
(
file
)
file
.
close
()
file
.
close
()
if
verbose
:
print
" You will update this :"
print
open
(
self
.
config
.
upgrade_file
).
read
()
if
dry_run
:
if
dry_run
:
return
return
self
.
_upload
(
self
.
config
.
upgrade_file
)
if
get_yes_no
(
"Do you want to continue? "
):
self
.
_upload
(
self
.
config
.
upgrade_file
)
def
update
(
self
,
reboot
=
None
,
upgrade
=
None
):
def
update
(
self
,
reboot
=
None
,
upgrade
=
None
):
self
.
load
()
if
reboot
is
None
and
upgrade
is
None
:
if
reboot
is
None
and
upgrade
is
None
:
return
return
if
not
self
.
current_state
.
has_section
(
'system'
):
if
not
self
.
current_state
.
has_section
(
'system'
):
...
@@ -182,6 +195,20 @@ class Signature:
...
@@ -182,6 +195,20 @@ class Signature:
self
.
current_state
.
write
(
current_state_file
)
self
.
current_state
.
write
(
current_state_file
)
current_state_file
.
close
()
current_state_file
.
close
()
def
get_signature_dict
(
self
):
""" Convert Next state info into a dict """
map_dict
=
{}
for
key
in
self
.
next_state
.
sections
():
if
key
==
"system"
:
continue
def
clean_list
(
l
):
return
[
x
.
strip
()
for
x
in
l
.
split
(
'
\
n
'
)
if
x
.
strip
()
!=
''
]
map_dict
[
key
]
=
{}
for
entry
in
self
.
next_state
.
options
(
key
):
map_dict
[
key
][
entry
]
=
clean_list
(
self
.
next_state
.
get
(
key
,
entry
))
return
map_dict
def
_read_state
(
self
,
state
,
name
):
def
_read_state
(
self
,
state
,
name
):
""" Extract information from config file """
""" Extract information from config file """
if
not
state
.
has_section
(
'system'
):
if
not
state
.
has_section
(
'system'
):
...
@@ -196,10 +223,10 @@ class Signature:
...
@@ -196,10 +223,10 @@ class Signature:
self
.
current_state
=
ConfigParser
.
RawConfigParser
()
self
.
current_state
=
ConfigParser
.
RawConfigParser
()
self
.
current_state
.
read
(
self
.
config
.
srv_file
)
self
.
current_state
.
read
(
self
.
config
.
srv_file
)
self
.
next_state
=
ConfigParser
.
Raw
ConfigParser
()
self
.
next_state
=
ConfigParser
.
ConfigParser
()
self
.
next_state
.
read
(
self
.
download
())
self
.
next_state
.
read
(
self
.
download
())
self
.
reboot
=
self
.
_read_state
(
self
.
next_state
,
"
upgrade
"
)
self
.
reboot
=
self
.
_read_state
(
self
.
next_state
,
"
reboot
"
)
self
.
upgrade
=
self
.
_read_state
(
self
.
next_state
,
"upgrade"
)
self
.
upgrade
=
self
.
_read_state
(
self
.
next_state
,
"upgrade"
)
self
.
last_reboot
=
self
.
_read_state
(
self
.
current_state
,
"reboot"
)
self
.
last_reboot
=
self
.
_read_state
(
self
.
current_state
,
"reboot"
)
self
.
last_upgrade
=
self
.
_read_state
(
self
.
current_state
,
"upgrade"
)
self
.
last_upgrade
=
self
.
_read_state
(
self
.
current_state
,
"upgrade"
)
slapos/package/update.py
View file @
b9ac2fef
...
@@ -37,7 +37,7 @@ import subprocess as sub
...
@@ -37,7 +37,7 @@ import subprocess as sub
import
sys
import
sys
import
tempfile
import
tempfile
from
signature
import
Signature
from
signature
import
Signature
from
base_promise
import
BasePromise
# create console handler and set level to warning
# create console handler and set level to warning
ch
=
logging
.
StreamHandler
()
ch
=
logging
.
StreamHandler
()
...
@@ -97,48 +97,78 @@ class Upgrader:
...
@@ -97,48 +97,78 @@ class Upgrader:
# add ch to logger
# add ch to logger
self
.
logger
.
addHandler
(
ch
)
self
.
logger
.
addHandler
(
ch
)
def
checkConsistency
(
self
,
*
args
,
**
kw
):
def
fixConsistency
(
self
,
signature
,
upgrade
=
0
,
reboot
=
0
,
boot
=
0
,
**
kw
):
print
"CHECK CONSISTENCY %s"
%
((
args
,
kw
),)
print
upgrade
,
reboot
,
boot
def
run
(
self
):
"""
Will fetch information from web and update and/or reboot
machine if needed
"""
today
=
datetime
.
date
.
today
().
isoformat
()
today
=
datetime
.
date
.
today
().
isoformat
()
if
upgrade
and
boot
:
signature
.
update
(
reboot
=
today
,
upgrade
=
today
)
if
upgrade
:
signature
.
update
(
upgrade
=
today
)
elif
reboot
:
signature
.
update
(
reboot
=
today
)
else
:
raise
ValueError
(
"You need upgrade and/or reboot when invoke fixConsistency!"
)
if
upgrade
:
pkgmanager
=
BasePromise
()
configuration_dict
=
signature
.
get_signature_dict
()
for
entry
in
configuration_dict
:
signature_list
=
configuration_dict
[
entry
].
get
(
"signature-list"
)
if
pkgmanager
.
matchSignatureList
(
signature_list
):
print
"Upgrade FOUND!!!! %s "
%
entry
upgrade_goal
=
configuration_dict
[
entry
]
break
repository_tuple_list
=
[]
for
repository
in
upgrade_goal
[
'repository-list'
]:
alias
,
url
=
repository
.
split
(
"="
)
repository_tuple_list
.
append
((
alias
.
strip
(),
url
.
strip
()))
pkgmanager
.
update
(
repository_tuple_list
,
upgrade_goal
[
'filter-package-list'
])
def
checkConsistency
(
self
,
fixit
=
0
,
**
kw
):
# Get configuration
# Get configuration
signature
=
Signature
(
self
.
config
)
signature
=
Signature
(
self
.
config
)
signature
.
load
()
signature
.
load
()
self
.
logger
.
debug
(
"Expected Reboot early them %s"
%
signature
.
reboot
)
self
.
logger
.
debug
(
"Expected Reboot early them %s"
%
signature
.
reboot
)
self
.
logger
.
debug
(
"Expected Upgrade early them %s"
%
signature
.
upgrade
)
self
.
logger
.
debug
(
"Expected Upgrade early them %s"
%
signature
.
upgrade
)
self
.
logger
.
debug
(
"Last reboot : %s"
%
signature
.
last_reboot
)
self
.
logger
.
debug
(
"Last reboot : %s"
%
signature
.
last_reboot
)
self
.
logger
.
debug
(
"Last upgrade : %s"
%
signature
.
last_upgrade
)
self
.
logger
.
debug
(
"Last upgrade : %s"
%
signature
.
last_upgrade
)
if
signature
.
upgrade
>
datetime
.
date
.
today
():
self
.
logger
.
debug
(
"Upgrade will happens on %s"
%
signature
.
upgrade
)
#return
# Check if run for first time
# Check if run for first time
if
signature
.
last_reboot
is
None
:
if
signature
.
last_reboot
is
None
:
if
not
self
.
config
.
dry_run
:
if
fixit
:
signature
.
update
(
reboot
=
today
,
upgrade
=
today
)
# Purge repositories list and add new ones
self
.
fixConsistency
(
signature
,
upgrade
=
1
,
boot
=
1
)
# Purge repositories list and add new ones
self
.
checkConsistency
(
fixit
=
not
self
.
config
.
dry_run
)
else
:
else
:
if
signature
.
last_upgrade
<
signature
.
upgrade
:
if
signature
.
last_upgrade
<
signature
.
upgrade
:
# Purge repositories list and add new ones
# Purge repositories list and add new ones
if
not
self
.
config
.
dry_run
:
if
fixit
:
signature
.
update
(
upgrade
=
today
)
self
.
fixConsistency
(
signature
,
upgrade
=
1
)
self
.
checkConsistency
(
fixit
=
not
self
.
config
.
dry_run
)
else
:
else
:
logger
.
info
(
"Your system is up to date"
)
logger
.
info
(
"Your system is up to date"
)
if
signature
.
last_reboot
<
signature
.
reboot
:
if
signature
.
last_reboot
<
signature
.
reboot
:
if
not
self
.
config
.
dry_run
:
if
not
self
.
config
.
dry_run
:
s
ignature
.
update
(
reboot
=
today
)
s
elf
.
fixConsistency
(
signature
,
reboot
=
1
)
else
:
else
:
self
.
logger
.
debug
(
"Dry run: Rebooting required."
)
self
.
logger
.
debug
(
"Dry run: Rebooting required."
)
def
run
(
self
):
"""
Will fetch information from web and update and/or reboot
machine if needed
"""
self
.
checkConsistency
(
fixit
=
not
self
.
config
.
dry_run
)
def
main
():
def
main
():
"""Update computer and slapos"""
"""Update computer and slapos"""
usage
=
"usage: %s [options] "
%
sys
.
argv
[
0
]
usage
=
"usage: %s [options] "
%
sys
.
argv
[
0
]
...
@@ -147,6 +177,5 @@ def main():
...
@@ -147,6 +177,5 @@ def main():
upgrader
.
run
()
upgrader
.
run
()
sys
.
exit
()
sys
.
exit
()
if
__name__
==
'__main__'
:
if
__name__
==
'__main__'
:
main
()
main
()
slapos/package/upload_key.py
View file @
b9ac2fef
...
@@ -36,6 +36,10 @@ import sys
...
@@ -36,6 +36,10 @@ import sys
from
update
import
Config
from
update
import
Config
from
signature
import
Signature
from
signature
import
Signature
def
do_upgrade
(
config
):
signature
=
Signature
(
config
)
signature
.
upload
(
dry_run
=
config
.
dry_run
)
class
Parser
(
OptionParser
):
class
Parser
(
OptionParser
):
"""
"""
...
@@ -84,28 +88,10 @@ def get_yes_no(prompt):
...
@@ -84,28 +88,10 @@ def get_yes_no(prompt):
if
answer
.
upper
()
in
[
'N'
,
'NO'
]:
if
answer
.
upper
()
in
[
'N'
,
'NO'
]:
return
False
return
False
def
new_upgrade
(
config
):
signature
=
Signature
(
config
)
signature
.
upload
(
dry_run
=
1
)
print
" You will update this :"
print
open
(
config
.
upgrade_file
).
read
()
if
not
get_yes_no
(
"Do you want to continue? "
):
sys
.
exit
(
0
)
if
not
config
.
dry_run
:
print
"Uploading..."
signature
.
upload
()
def
main
():
def
main
():
"""Upload file to update computer and slapos"""
"""Upload file to update computer and slapos"""
usage
=
"usage: [options] "
usage
=
"usage: [options] "
# Parse arguments
# Parse arguments
config
=
Config
(
Parser
(
usage
=
usage
).
check_args
())
config
=
Config
(
Parser
(
usage
=
usage
).
check_args
())
config
.
srv_file
=
"/srv/slapupdate"
do_upgrade
(
config
)
new_upgrade
(
config
)
sys
.
exit
()
sys
.
exit
()
if
__name__
==
'__main__'
:
main
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment