Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
slapos.core
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
17
Merge Requests
17
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Jobs
Commits
Open sidebar
nexedi
slapos.core
Commits
8bbb4f1d
Commit
8bbb4f1d
authored
Nov 26, 2018
by
Jérome Perrin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
standalone: implementation of IStandaloneSlapOS
parent
4eaf2efb
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
824 additions
and
1 deletion
+824
-1
slapos/slap/standalone.py
slapos/slap/standalone.py
+560
-0
slapos/tests/interface.py
slapos/tests/interface.py
+4
-1
slapos/tests/standalone.py
slapos/tests/standalone.py
+260
-0
No files found.
slapos/slap/standalone.py
0 → 100644
View file @
8bbb4f1d
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2018 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import
os
import
textwrap
from
six.moves
import
xmlrpc_client
as
xmlrpclib
import
hashlib
import
logging
import
time
import
sys
import
socket
import
errno
from
contextlib
import
closing
try
:
import
subprocess32
as
subprocess
except
ImportError
:
import
subprocess
import
xml_marshaller
import
zope.interface
import
supervisor.xmlrpc
import
psutil
from
.interface.slap
import
IException
from
.interface.slap
import
ISupply
from
.interface.slap
import
IRequester
from
.interface.slap
import
IStandaloneSlapOS
from
.slap
import
slap
from
.slap
import
Supply
from
.slap
import
ConnectionError
@
zope
.
interface
.
implementer
(
IException
)
class
SlapOSNodeCommandError
(
Exception
):
"""Exception raised when running a SlapOS Node command failed.
"""
@
zope
.
interface
.
implementer
(
IException
)
class
PathTooDeepError
(
Exception
):
"""Exception raised when path is too deep to create an unix socket.
"""
class
ConfigWriter
(
object
):
"""Base class for an object writing a config file.
"""
def
__init__
(
self
,
standalone_slapos
):
self
.
_standalone_slapos
=
standalone_slapos
def
writeConfig
(
self
,
path
):
NotImplemented
class
SupervisorConfigWriter
(
ConfigWriter
):
"""Write supervisor configuration at etc/supervisor.conf
"""
def
_getProgramConfig
(
self
,
program_name
,
command
):
"""Format a supervisor program block.
"""
return
textwrap
.
dedent
(
"""
\
[program:{program_name}]
command = {command}
autostart = false
autorestart = false
startretries = 1
redirect_stderr = true
stdout_logfile_maxbytes = 5MB
stdout_logfile_backups = 10
"""
.
format
(
**
locals
()))
def
_getSupervisorConfigParts
(
self
):
"""Iterator on parts of formatted config.
"""
standalone_slapos
=
self
.
_standalone_slapos
yield
textwrap
.
dedent
(
"""
[unix_http_server]
file = {standalone_slapos._supervisor_socket}
[supervisorctl]
serverurl = unix://{standalone_slapos._supervisor_socket}
[supervisord]
logfile = {standalone_slapos._supervisor_log}
pidfile = {standalone_slapos._supervisor_pid}
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[program:slapos-proxy]
command = slapos proxy start --cfg {standalone_slapos._slapos_config} --verbose
"""
.
format
(
**
locals
()))
for
program
,
program_config
in
standalone_slapos
.
_slapos_commands
.
items
():
yield
self
.
_getProgramConfig
(
program
,
program_config
[
'command'
].
format
(
self
=
standalone_slapos
,
debug_args
=
''
,
))
def
writeConfig
(
self
,
config_file_path
):
with
open
(
config_file_path
,
'w'
)
as
f
:
for
part
in
self
.
_getSupervisorConfigParts
():
f
.
write
(
part
)
class
SlapOSConfigWriter
(
ConfigWriter
):
"""Write slapos configuration at etc/slapos.cfg
"""
def
writeConfig
(
self
,
config_file_path
):
standalone_slapos
=
self
.
_standalone_slapos
with
open
(
config_file_path
,
'w'
)
as
f
:
f
.
write
(
textwrap
.
dedent
(
"""
[slapos]
software_root = {standalone_slapos._software_root}
instance_root = {standalone_slapos._instance_root}
master_url = {standalone_slapos._master_url}
computer_id = {standalone_slapos._computer_id}
root_check = False
pidfile_software = {standalone_slapos._instance_pid}
pidfile_instance = {standalone_slapos._software_pid}
pidfile_report = {standalone_slapos._report_pid}
[slapproxy]
host = {standalone_slapos._server_ip}
port = {standalone_slapos._server_port}
database_uri = {standalone_slapos._proxy_database}
"""
.
format
(
**
locals
())))
@
zope
.
interface
.
implementer
(
IStandaloneSlapOS
,
ISupply
,
IRequester
)
class
StandaloneSlapOS
(
object
):
"""A SlapOS that can be embedded in other applications, also useful for testing.
This plays the role of an `IComputer` where users of classes
implementing this interface can install software, create partitions
and access parameters of the running partitions.
Extends the existing `IRequester` and `ISupply`, with the
special behavior that `IRequester.request` and `ISupply.supply` will
automatically use the embedded computer.
TODO: a register method to create new instance and refuse reusing instance ? or do this in format ?
"""
def
__init__
(
self
,
base_directory
,
server_ip
,
server_port
,
computer_id
=
'local'
):
"""Constructor, creates a standalone slapos in `base_directory`.
Arguments:
* `base_directory` -- the directory which will contain softwares and instances.
* `server_ip`, `server_port` -- the address this SlapOS proxy will listen to.
* `computer_id` -- the id of this computer.
Error cases:
* `PathTooDeepError` when `base_directory` is too deep. Because of limitation
with the length of paths of UNIX sockets, too deep paths cannot be used.
Note that once slapns work is integrated, this should not be an issue anymore.
"""
self
.
_logger
=
logging
.
getLogger
(
__name__
)
# slapos proxy address
self
.
_server_ip
=
server_ip
self
.
_server_port
=
server_port
self
.
_master_url
=
"http://{server_ip}:{server_port}"
.
format
(
**
locals
())
self
.
_base_directory
=
base_directory
self
.
_slapos_commands
=
{
'slapos-node-software'
:
{
'command'
:
'slapos node software --cfg {self._slapos_config} --all {debug_args}'
,
'debug_args'
:
'--buildout-debug'
,
},
'slapos-node-instance'
:
{
'command'
:
'slapos node instance --cfg {self._slapos_config} --all {debug_args}'
,
'debug_args'
:
'--buildout-debug'
,
},
'slapos-node-report'
:
{
'command'
:
'slapos node report --cfg {self._slapos_config} {debug_args}'
,
'debug_args'
:
''
,
}
}
self
.
_computer_id
=
computer_id
self
.
_computer
=
None
self
.
_slap
=
slap
()
self
.
_slap
.
initializeConnection
(
self
.
_master_url
)
self
.
_initBaseDirectory
()
def
_initBaseDirectory
(
self
):
"""Create the directory after checking it's not too deep.
"""
base_directory
=
self
.
_base_directory
self
.
_software_root
=
os
.
path
.
join
(
base_directory
,
'soft'
)
self
.
_instance_root
=
os
.
path
.
join
(
base_directory
,
'inst'
)
# To prevent error: Cannot open an HTTP server: socket.error reported
# AF_UNIX path too long This `working_directory` should not be too deep.
# Socket path is 108 char max on linux
# https://github.com/torvalds/linux/blob/3848ec5/net/unix/af_unix.c#L234-L238
# Supervisord socket name contains the pid number, which is why we add
# .xxxxxxx in this check.
if
len
(
os
.
path
.
join
(
self
.
_instance_root
,
'supervisord.socket.xxxxxxx'
))
>
108
:
raise
PathTooDeepError
(
'working directory ( {base_directory} ) is too deep'
.
format
(
**
locals
()))
if
not
os
.
path
.
exists
(
base_directory
):
os
.
mkdir
(
base_directory
)
etc_directory
=
os
.
path
.
join
(
base_directory
,
'etc'
)
if
not
os
.
path
.
exists
(
etc_directory
):
os
.
mkdir
(
etc_directory
)
self
.
_supervisor_config
=
os
.
path
.
join
(
etc_directory
,
'supervisord.conf'
)
self
.
_slapos_config
=
os
.
path
.
join
(
etc_directory
,
'slapos.cfg'
)
var_directory
=
os
.
path
.
join
(
base_directory
,
'var'
)
if
not
os
.
path
.
exists
(
var_directory
):
os
.
mkdir
(
var_directory
)
self
.
_proxy_database
=
os
.
path
.
join
(
var_directory
,
'proxy.db'
)
# for convenience, make a slapos command for this instance
bin_directory
=
os
.
path
.
join
(
base_directory
,
'bin'
)
if
not
os
.
path
.
exists
(
bin_directory
):
os
.
mkdir
(
bin_directory
)
self
.
_slapos_bin
=
os
.
path
.
join
(
bin_directory
,
'slapos'
)
log_directory
=
os
.
path
.
join
(
var_directory
,
'log'
)
if
not
os
.
path
.
exists
(
log_directory
):
os
.
mkdir
(
log_directory
)
self
.
_supervisor_log
=
os
.
path
.
join
(
log_directory
,
'supervisord.log'
)
run_directory
=
os
.
path
.
join
(
var_directory
,
'run'
)
if
not
os
.
path
.
exists
(
run_directory
):
os
.
mkdir
(
run_directory
)
self
.
_supervisor_pid
=
os
.
path
.
join
(
run_directory
,
'supervisord.pid'
)
self
.
_software_pid
=
os
.
path
.
join
(
run_directory
,
'slapos-node-software.pid'
)
self
.
_instance_pid
=
os
.
path
.
join
(
run_directory
,
'slapos-node-instance.pid'
)
self
.
_report_pid
=
os
.
path
.
join
(
run_directory
,
'slapos-node-report.pid'
)
self
.
_supervisor_socket
=
os
.
path
.
join
(
run_directory
,
'supervisord.sock'
)
SupervisorConfigWriter
(
self
).
writeConfig
(
self
.
_supervisor_config
)
SlapOSConfigWriter
(
self
).
writeConfig
(
self
.
_slapos_config
)
self
.
_writeSlaposCommand
()
self
.
_ensureSupervisordStarted
()
self
.
_ensureSlaposAvailable
()
def
_writeSlaposCommand
(
self
):
# XXX move to class ?
with
open
(
self
.
_slapos_bin
,
'w'
)
as
f
:
f
.
write
(
textwrap
.
dedent
(
"""
\
#!/bin/sh
SLAPOS_CONFIGURATION={self._slapos_config}
\
\
SLAPOS_CLIENT_CONFIGURATION=$SLAPOS_CONFIGURATION
\
\
exec slapos "$@"
"""
.
format
(
**
locals
())))
os
.
chmod
(
self
.
_slapos_bin
,
0o755
)
@
property
def
computer
(
self
):
"""Access the computer.
"""
if
self
.
_computer
is
None
:
self
.
_computer
=
self
.
_slap
.
registerComputer
(
self
.
_computer_id
)
return
self
.
_computer
@
property
def
software_directory
(
self
):
"""Path to software directory
"""
return
self
.
_software_root
@
property
def
instance_directory
(
self
):
"""Path to instance directory
"""
return
self
.
_instance_root
@
property
def
supervisor_rpc
(
self
):
"""A xmlrpc connection to control supervisor, use as a context manager.
Refer to http://supervisord.org/api.html for details of available methods.
"""
# xmlrpc over unix socket https://stackoverflow.com/a/11746051/7294664
return
xmlrpclib
.
ServerProxy
(
'http://slapos-standalone-supervisor'
,
transport
=
supervisor
.
xmlrpc
.
SupervisorTransport
(
None
,
None
,
serverurl
=
"unix://{self._supervisor_socket}"
.
format
(
**
locals
())))
def
format
(
self
,
partition_count
,
ipv4_address
,
ipv6_address
,
partition_base_name
=
"slappart"
):
"""Creates `partition_count` partitions.
All partitions have the same `ipv4_address` and `ipv6_address` and
use the current system user.
"""
for
path
in
(
self
.
_software_root
,
self
.
_instance_root
,
):
if
not
os
.
path
.
exists
(
path
):
os
.
mkdir
(
path
)
# prepare software directory
# create partitions and configure computer
for
i
in
range
(
partition_count
):
partition_reference
=
'%s%s'
%
(
partition_base_name
,
i
)
partition_path
=
os
.
path
.
join
(
self
.
_instance_root
,
partition_reference
)
if
not
(
os
.
path
.
exists
(
partition_path
)):
os
.
mkdir
(
partition_path
)
os
.
chmod
(
partition_path
,
0o750
)
self
.
computer
.
updateConfiguration
(
xml_marshaller
.
xml_marshaller
.
dumps
({
'address'
:
ipv4_address
,
'netmask'
:
'255.255.255.255'
,
'partition_list'
:
[
{
'address_list'
:
[{
'addr'
:
ipv4_address
,
'netmask'
:
'255.255.255.255'
},
{
'addr'
:
ipv6_address
,
'netmask'
:
'ffff:ffff:ffff::'
},],
'path'
:
partition_path
,
'reference'
:
partition_reference
,
'tap'
:
{
'name'
:
partition_reference
},}],
'reference'
:
self
.
_computer_id
,
'instance_root'
:
self
.
_instance_root
,
'software_root'
:
self
.
_software_root
}))
def
supply
(
self
,
software_url
,
computer_guid
=
None
,
state
=
"available"
):
"""Supply a software, see ISupply.supply
Software can only be supplied on this embedded computer.
"""
if
computer_guid
not
in
(
None
,
self
.
_computer_id
):
raise
ValueError
(
"Can only supply on embedded computer"
)
self
.
_slap
.
registerSupply
().
supply
(
software_url
,
self
.
_computer_id
,
state
=
state
,
)
def
request
(
self
,
software_release
,
software_type
,
partition_reference
,
shared
=
False
,
partition_parameter_kw
=
None
,
filter_kw
=
None
,
state
=
None
):
"""Request an instance, see IRequester.request
Instance can only be requested on this embedded computer.
"""
if
filter_kw
is
not
None
:
raise
ValueError
(
"Can only request on embedded computer"
)
if
shared
:
raise
ValueError
(
"Can not request shared instances"
)
return
self
.
_slap
.
registerOpenOrder
().
request
(
software_release
,
software_type
=
software_type
,
partition_reference
=
partition_reference
,
shared
=
shared
,
partition_parameter_kw
=
partition_parameter_kw
,
filter_kw
=
filter_kw
,
state
=
state
)
def
shutdown
(
self
):
"""Shutdown all services.
This methods blocks until services are stop or a timeout is reached.
Error cases:
* `RuntimeError` when supervisor refuses to shutdown.
"""
with
self
.
supervisor_rpc
as
rpc
:
rpc
.
supervisor
.
shutdown
()
# wait for supervisor to shutdown and free its port
for
i
in
range
(
10
):
with
closing
(
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_STREAM
))
as
s
:
try
:
s
.
connect
((
self
.
_server_ip
,
self
.
_server_port
))
except
socket
.
error
as
e
:
if
e
.
errno
==
errno
.
ECONNREFUSED
:
return
raise
time
.
sleep
(.
2
*
i
)
raise
RuntimeError
(
"Shutdown failed"
)
def
waitForSoftware
(
self
,
max_retry
=
0
,
debug
=
False
,
error_lines
=
30
):
"""Synchronously install or uninstall all softwares previously supplied/removed.
This method retries on errors. If after `max_retry` times there's
still an error, the error is raised, containing `error_lines` of output
from the buildout command.
If `debug` is true, buildout is executed in the foreground, with flags to
drop in a debugger session if error occurs.
Error cases:
* `SlapOSNodeCommandError` when buildout error while installing software.
* Unexpected `Exception` if unable to connect to embedded slap server.
"""
return
self
.
_runSlapOSCommand
(
'slapos-node-software'
,
max_retry
=
max_retry
,
debug
=
debug
,
)
def
waitForInstance
(
self
,
max_retry
=
0
,
debug
=
False
,
error_lines
=
30
):
"""Instantiate all partitions previously requested for start.
This method retries on errors. If after `max_retry` times there's
still an error, the error is raised, containing `error_lines` of output
from the buildout command.
If `debug` is true, buildout is executed in the foreground, with flags to
drop in a debugger session if error occurs.
Error cases:
* `SlapOSNodeCommandError` when buildout error while installing software.
* Unexpected `Exception` if unable to connect to embedded slap server.
"""
return
self
.
_runSlapOSCommand
(
'slapos-node-instance'
,
max_retry
=
max_retry
,
debug
=
debug
,
)
def
waitForReport
(
self
,
max_retry
=
0
,
debug
=
False
,
error_lines
=
30
):
"""Destroy all partitions previously requested for destruction.
This method retries on errors. If after `max_retry` times there's
still an error, the error is raised, containing `error_lines` of output
from the buildout command.
If `debug` is true, buildout is executed in the foreground, with flags to
drop in a debugger session if error occurs.
Error cases:
* `SlapOSNodeCommandError` when buildout error while installing software.
* Unexpected `Exception` if unable to connect to embedded slap server.
"""
return
self
.
_runSlapOSCommand
(
'slapos-node-report'
,
max_retry
=
max_retry
,
debug
=
debug
,
)
def
_runSlapOSCommand
(
self
,
command
,
max_retry
=
0
,
debug
=
False
,
error_lines
=
30
):
success
=
False
if
debug
:
prog
=
self
.
_slapos_commands
[
command
]
debug_args
=
prog
.
get
(
'debug_args'
,
''
)
return
subprocess
.
check_call
(
prog
[
'command'
].
format
(
**
locals
()),
shell
=
True
)
with
self
.
supervisor_rpc
as
server
:
retry
=
0
while
True
:
self
.
_logger
.
debug
(
"retry %s: starting %s"
,
retry
,
command
)
server
.
supervisor
.
startProcess
(
command
,
False
)
delay
=
0.3
while
True
:
self
.
_logger
.
debug
(
"retry %s: sleeping %s"
,
retry
,
delay
)
# we start waiting a short delay and increase the delay each loop,
# because when software is already built, this should return fast,
# but when we build a full software we don't need to poll the
# supervisor too often.
time
.
sleep
(
delay
)
delay
=
min
(
delay
*
1.2
,
30
)
process_info
=
server
.
supervisor
.
getProcessInfo
(
command
)
if
process_info
[
'statename'
]
in
(
'EXITED'
,
'FATAL'
):
self
.
_logger
.
debug
(
"SlapOS command finished %s"
%
process_info
)
if
process_info
[
'exitstatus'
]
==
0
:
return
if
retry
>=
max_retry
:
# get the last lines of output, at most `error_lines`. If
# these lines are long, the output may be truncated.
_
,
log_offset
,
_
=
server
.
supervisor
.
tailProcessStdoutLog
(
command
,
0
,
0
)
output
,
_
,
_
=
server
.
supervisor
.
tailProcessStdoutLog
(
command
,
log_offset
-
(
2
<<
13
),
2
<<
13
)
raise
SlapOSNodeCommandError
({
'output'
:
'
\
n
'
.
join
(
output
.
splitlines
()[
-
error_lines
:]),
'exitstatus'
:
process_info
[
'exitstatus'
],
})
break
retry
+=
1
def
_ensureSupervisordStarted
(
self
):
if
os
.
path
.
exists
(
self
.
_supervisor_pid
):
with
open
(
self
.
_supervisor_pid
,
'r'
)
as
f
:
try
:
pid
=
int
(
f
.
read
())
except
(
ValueError
,
TypeError
)
as
e
:
self
.
_logger
.
debug
(
"Error reading supervisor pid from file, assuming it's not running"
)
else
:
process
=
psutil
.
Process
(
pid
)
if
process
.
name
()
==
'supervisord'
:
# OK looks already running
return
self
.
_logger
.
debug
(
"Supervisor pid file seem stale"
)
# start new supervisord
subprocess
.
check_call
(
[
'supervisord'
],
cwd
=
self
.
_base_directory
,
)
def
_ensureSlaposAvailable
(
self
):
# Wait for proxy to accept connections
retry
=
0
while
True
:
time
.
sleep
(
1
)
try
:
# Call a method to ensure connection to master can be established
self
.
computer
.
getComputerPartitionList
()
except
ConnectionError
as
e
:
retry
+=
1
if
retry
>=
60
:
raise
self
.
_logger
.
debug
(
"Proxy still not started %s, retrying"
,
e
)
else
:
break
slapos/tests/interface.py
View file @
8bbb4f1d
...
...
@@ -30,6 +30,8 @@ from zope.interface.verify import verifyClass
import
zope.interface
from
six
import
class_types
from
slapos
import
slap
# XXX ???
import
slapos.slap.standalone
def
getOnlyImplementationAssertionMethod
(
klass
,
method_list
):
"""Returns method which verifies if a klass only implements its interfaces"""
...
...
@@ -94,7 +96,8 @@ class TestInterface(unittest.TestCase):
"""
# add methods to test class
generateTestMethodListOnClass
(
TestInterface
,
slap
)
generateTestMethodListOnClass
(
TestInterface
,
slapos
.
slap
)
generateTestMethodListOnClass
(
TestInterface
,
slapos
.
slap
.
standalone
)
if
__name__
==
'__main__'
:
unittest
.
main
()
slapos/tests/standalone.py
0 → 100644
View file @
8bbb4f1d
##############################################################################
#
# Copyright (c) 2018 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import
unittest
import
os
import
tempfile
import
textwrap
import
shutil
import
hashlib
import
socket
import
errno
from
contextlib
import
closing
import
psutil
from
slapos.slap.standalone
import
StandaloneSlapOS
from
slapos.slap.standalone
import
SlapOSNodeCommandError
SLAPOS_TEST_WORKING_DIR
=
os
.
environ
[
'SLAPOS_TEST_WORKING_DIR'
]
SLAPOS_TEST_IPV4
=
os
.
environ
[
'SLAPOS_TEST_IPV4'
]
SLAPOS_TEST_IPV6
=
os
.
environ
[
'SLAPOS_TEST_IPV6'
]
SLAPOS_TEST_PORT
=
int
(
os
.
environ
[
'SLAPOS_TEST_PORT'
])
def
checkPortIsFree
():
"""Sanity check that we did not leak a process listening on this port.
"""
with
closing
(
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_STREAM
))
as
s
:
try
:
s
.
connect
((
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_PORT
))
raise
RuntimeError
(
"Port needed for tests is already in use"
)
except
socket
.
error
as
e
:
if
e
.
errno
==
errno
.
ECONNREFUSED
:
return
raise
class
TestSlapOSStandaloneSetup
(
unittest
.
TestCase
):
def
setUp
(
self
):
checkPortIsFree
()
def
test_format
(
self
):
working_dir
=
tempfile
.
mkdtemp
(
prefix
=
__name__
)
self
.
addCleanup
(
shutil
.
rmtree
,
working_dir
)
standalone
=
StandaloneSlapOS
(
working_dir
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_PORT
)
self
.
addCleanup
(
standalone
.
shutdown
)
standalone
.
format
(
3
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_IPV6
)
self
.
assertTrue
(
os
.
path
.
exists
(
standalone
.
software_directory
))
self
.
assertTrue
(
os
.
path
.
exists
(
standalone
.
instance_directory
))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
standalone
.
instance_directory
,
'slappart0'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
standalone
.
instance_directory
,
'slappart1'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
standalone
.
instance_directory
,
'slappart2'
)))
def
test_two_instance_from_same_directory
(
self
):
working_dir
=
tempfile
.
mkdtemp
(
prefix
=
__name__
)
self
.
addCleanup
(
shutil
.
rmtree
,
working_dir
)
standalone1
=
StandaloneSlapOS
(
working_dir
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_PORT
)
self
.
addCleanup
(
standalone1
.
shutdown
)
standalone2
=
StandaloneSlapOS
(
working_dir
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_PORT
)
class
SlapOSStandaloneTestCase
(
unittest
.
TestCase
):
def
setUp
(
self
):
checkPortIsFree
()
working_dir
=
tempfile
.
mkdtemp
(
prefix
=
__name__
)
self
.
addCleanup
(
shutil
.
rmtree
,
working_dir
)
self
.
standalone
=
StandaloneSlapOS
(
working_dir
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_PORT
)
self
.
addCleanup
(
self
.
standalone
.
shutdown
)
self
.
standalone
.
format
(
1
,
SLAPOS_TEST_IPV4
,
SLAPOS_TEST_IPV6
)
class
TestSlapOSStandaloneSoftware
(
SlapOSStandaloneTestCase
):
def
test_install_software
(
self
):
with
tempfile
.
NamedTemporaryFile
(
suffix
=
"-%s.cfg"
%
self
.
id
())
as
f
:
f
.
write
(
textwrap
.
dedent
(
'''
[buildout]
parts = instance
[instance]
recipe = plone.recipe.command==1.1
command = touch ${buildout:directory}/instance.cfg
'''
).
encode
())
f
.
flush
()
self
.
standalone
.
supply
(
f
.
name
)
self
.
standalone
.
waitForSoftware
()
software_hash
=
hashlib
.
md5
(
f
.
name
.
encode
()).
hexdigest
()
software_installation_path
=
os
.
path
.
join
(
self
.
standalone
.
software_directory
,
software_hash
)
self
.
assertTrue
(
os
.
path
.
exists
(
software_installation_path
))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
software_installation_path
,
'bin'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
software_installation_path
,
'parts'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
software_installation_path
,
'.completed'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
software_installation_path
,
'instance.cfg'
)))
# destroy
self
.
standalone
.
supply
(
f
.
name
,
state
=
'destroyed'
)
self
.
standalone
.
waitForSoftware
()
self
.
assertFalse
(
os
.
path
.
exists
(
software_installation_path
))
def
test_install_software_failure
(
self
):
with
tempfile
.
NamedTemporaryFile
(
suffix
=
"-%s.cfg"
%
self
.
id
())
as
f
:
f
.
write
(
textwrap
.
dedent
(
'''
[buildout]
parts = error
[error]
recipe = plone.recipe.command==1.1
command = bash -c "exit 123"
stop-on-error = true
'''
).
encode
())
f
.
flush
()
self
.
standalone
.
supply
(
f
.
name
)
with
self
.
assertRaises
(
SlapOSNodeCommandError
)
as
e
:
self
.
standalone
.
waitForSoftware
()
self
.
assertEqual
(
1
,
e
.
exception
.
args
[
0
][
'exitstatus'
])
self
.
assertIn
(
"Error: Non zero exit code (123) while running command."
,
e
.
exception
.
args
[
0
][
'output'
])
class
TestSlapOSStandaloneInstance
(
SlapOSStandaloneTestCase
):
def
test_request_instance
(
self
):
with
tempfile
.
NamedTemporaryFile
(
suffix
=
"-%s.cfg"
%
self
.
id
())
as
f
:
# This is a minimal / super fast buildout that's compatible with slapos.
# We don't want to install slapos.cookbook because installation takes too
# much time, so we use simple plone.recipe.command and shell.
# This buildout create an instance with two parts:
# check_parameter: that checks that the requested parameter is set
# publish: that publish some parameters so that we can assert it's published.
software_url
=
f
.
name
f
.
write
(
textwrap
.
dedent
(
'''
[buildout]
parts = instance
[instance]
recipe = plone.recipe.command==1.1
stop-on-error = true
# we use @@DOLLAR@@{section:option} for what will become instance substitutions
command = sed -e s/@@DOLLAR@@/$/g <<EOF > ${buildout:directory}/instance.cfg
[buildout]
parts = check_parameter publish
eggs-directory = ${buildout:eggs-directory}
[check_parameter]
# check we were requested with request=parameter ( as a way to test
# request parameters are sent )
recipe = plone.recipe.command==1.1
stop-on-error = true
command =
\
\
curl '@@DOLLAR@@{slap-connection:server-url}/registerComputerPartition?computer_reference=@@DOLLAR@@{slap-connection:computer-id}&computer_partition_reference=@@DOLLAR@@{slap-connection:partition-id}'
\
\
| grep '<string>request</string><string>parameter</string>'
[publish]
# touch a file to check instance exists and publish a hardcoded parameter
recipe = plone.recipe.command==1.1
stop-on-error = true
command =
\
\
touch instance.check
\
\
&& curl -X POST @@DOLLAR@@{slap-connection:server-url}/setComputerPartitionConnectionXml
\
\
-d computer_id=@@DOLLAR@@{slap-connection:computer-id}
\
\
-d computer_partition_id=@@DOLLAR@@{slap-connection:partition-id}
\
\
-d connection_xml='<dictionary><string>published</string><string>parameter</string></dictionary>'
EOF
'''
).
encode
())
f
.
flush
()
self
.
standalone
.
supply
(
software_url
)
self
.
standalone
.
waitForSoftware
()
self
.
standalone
.
request
(
software_url
,
'default'
,
'instance'
,
partition_parameter_kw
=
{
'request'
:
'parameter'
})
self
.
standalone
.
waitForInstance
()
# check published parameters
partition
=
self
.
standalone
.
request
(
software_url
,
'default'
,
'instance'
,
partition_parameter_kw
=
{
'request'
:
'parameter'
})
self
.
assertEqual
(
{
'published'
:
'parameter'
},
partition
.
getConnectionParameterDict
()
)
# check instance files
parition_directory
=
os
.
path
.
join
(
self
.
standalone
.
instance_directory
,
'slappart0'
)
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
parition_directory
,
'.installed.cfg'
)))
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
parition_directory
,
'instance.check'
)))
# delete instance
self
.
standalone
.
request
(
software_url
,
'default'
,
'instance'
,
partition_parameter_kw
=
{
'partition'
:
'parameter'
},
state
=
'destroyed'
,
)
self
.
standalone
.
waitForInstance
()
# instanciate does nothing, it will be deleted with `report`
self
.
assertTrue
(
os
.
path
.
exists
(
os
.
path
.
join
(
parition_directory
,
'instance.check'
)))
self
.
standalone
.
waitForReport
()
self
.
assertFalse
(
os
.
path
.
exists
(
os
.
path
.
join
(
parition_directory
,
'instance.check'
)))
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