Commit b9485921 authored by Rafael Monnerat's avatar Rafael Monnerat

Update Release Candidate

parents 92c1115a 1c492ab2
{ {
"$schema": "http://json-schema.org/draft-04/schema", "$schema": "http://json-schema.org/draft-04/schema",
"properties": { "properties": {
"custom_domain": {
"description": "Custom Domain to use for the website",
"pattern": "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$",
"title": "Custom Domain",
"type": "string"
},
"url": {
"description": "Url of the backend",
"pattern": "^(http|https|ftp)://",
"title": "Backend URL",
"type": "string"
},
"type": {
"default": "",
"description": "Type of slave. If redirect, the slave will redirect to the given url. If zope, the rewrite rules will be compatible with Virtual Host Monster. Implemented are default, zope, redirect, notebook and websocket, not implemneted is eventsource.",
"enum": [
"",
"zope",
"redirect",
"notebook",
"websocket",
"eventsource"
],
"title": "Backend Type",
"type": "string"
},
"path": {
"default": "",
"description": "Path to proxy to in the backend",
"title": "type:zope Backend Path",
"type": "string"
},
"enable_cache": {
"default": "false",
"description": "If set to true, http caching server (Apache Traffic Server) will be used between frontend Caddy and backend",
"enum": [
"false",
"true"
],
"title": "Enable Cache",
"type": "string"
},
"https-only": {
"default": "false",
"description": "If set to true, http requests will be redirected to https",
"enum": [
"false",
"true"
],
"title": "HTTPS Only",
"type": "string"
},
"caddy_custom_http": { "caddy_custom_http": {
"default": "", "default": "",
"description": "Raw http configuration in python template format. Your site will be rejected if you use it without notification and approval of frontend administrators", "description": "Raw http configuration in python template format. Your site will be rejected if you use it without notification and approval of frontend administrators",
...@@ -15,12 +67,6 @@ ...@@ -15,12 +67,6 @@
"title": "HTTPS configuration", "title": "HTTPS configuration",
"type": "string" "type": "string"
}, },
"custom_domain": {
"description": "Custom Domain to use for the website",
"pattern": "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$",
"title": "Custom Domain",
"type": "string"
},
"default-path": { "default-path": {
"default": "", "default": "",
"description": "Provide default path to redirect user to when user access / (the site root)", "description": "Provide default path to redirect user to when user access / (the site root)",
...@@ -63,26 +109,6 @@ ...@@ -63,26 +109,6 @@
"title": "Enable HTTP2 Protocol", "title": "Enable HTTP2 Protocol",
"type": "string" "type": "string"
}, },
"enable_cache": {
"default": "false",
"description": "If set to true, http caching server (Apache Traffic Server) will be used between frontend Caddy and backend",
"enum": [
"false",
"true"
],
"title": "Enable Cache",
"type": "string"
},
"https-only": {
"default": "false",
"description": "If set to true, http requests will be redirected to https",
"enum": [
"false",
"true"
],
"title": "HTTPS Only",
"type": "string"
},
"https-url": { "https-url": {
"description": "HTTPS Url of the backend if it is diferent from url parameter", "description": "HTTPS Url of the backend if it is diferent from url parameter",
"pattern": "^(http|https|ftp)://", "pattern": "^(http|https|ftp)://",
...@@ -101,12 +127,6 @@ ...@@ -101,12 +127,6 @@
"title": "IPv6 Address to Monitor Packet Lost", "title": "IPv6 Address to Monitor Packet Lost",
"type": "string" "type": "string"
}, },
"path": {
"default": "",
"description": "Path to proxy to in the backend",
"title": "type:zope Backend Path",
"type": "string"
},
"websocket-path-list": { "websocket-path-list": {
"default": "", "default": "",
"description": "Space separated list of path to the websocket application. If not set the whole slave will be websocket, if set then / will be HTTP, and /<websocket-path> will be WSS. In order to have ' ' in the space use '%20'", "description": "Space separated list of path to the websocket application. If not set the whole slave will be websocket, if set then / will be HTTP, and /<websocket-path> will be WSS. In order to have ' ' in the space use '%20'",
...@@ -183,26 +203,6 @@ ...@@ -183,26 +203,6 @@
"title": "SSL Backend Authority's Certificate", "title": "SSL Backend Authority's Certificate",
"type": "string" "type": "string"
}, },
"type": {
"default": "",
"description": "Type of slave. If redirect, the slave will redirect to the given url. If zope, the rewrite rules will be compatible with Virtual Host Monster. Implemented are default, zope, redirect, notebook and websocket, not implemneted is eventsource.",
"enum": [
"",
"zope",
"redirect",
"notebook",
"websocket",
"eventsource"
],
"title": "Backend Type",
"type": "string"
},
"url": {
"description": "Url of the backend",
"pattern": "^(http|https|ftp)://",
"title": "Backend URL",
"type": "string"
},
"virtualhostroot-http-port": { "virtualhostroot-http-port": {
"default": 80, "default": 80,
"description": "Port where http requests to frontend will be redirected.", "description": "Port where http requests to frontend will be redirected.",
......
...@@ -28,7 +28,8 @@ from setuptools import setup, find_packages ...@@ -28,7 +28,8 @@ from setuptools import setup, find_packages
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.erp5' name = 'slapos.test.erp5'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -25,15 +25,24 @@ ...@@ -25,15 +25,24 @@
# #
############################################################################## ##############################################################################
import json
import os import os
import unittest
import logging
if os.environ.get('DEBUG'): from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
raise ValueError("Don't set DEBUG - it breaks postfix compilation - set SLAPOS_TEST_DEBUG instead.")
debug_mode = os.environ.get('SLAPOS_TEST_DEBUG') setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
# for development: debugging logs and install Ctrl+C handler os.path.abspath(
if debug_mode: os.path.join(os.path.dirname(__file__), '..', '..', 'software.cfg')))
logging.basicConfig(level=logging.DEBUG)
unittest.installHandler()
class ERP5InstanceTestCase(SlapOSInstanceTestCase):
"""ERP5 base test case
"""
# ERP5 instanciation needs to run several times before being ready, as
# the root instance request more instances.
instance_max_retry = 7 # XXX how many times ?
def getRootPartitionConnectionParameterDict(self):
"""Return the output paramters from the root partition"""
return json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
...@@ -29,28 +29,15 @@ import os ...@@ -29,28 +29,15 @@ import os
import json import json
import glob import glob
import urlparse import urlparse
import logging
import socket import socket
import time import time
import psutil import psutil
import requests import requests
from utils import SlapOSInstanceTestCase from . import ERP5InstanceTestCase
from . import setUpModule
setUpModule # pyflakes
class ERP5TestCase(SlapOSInstanceTestCase):
"""Test the remote driver on a minimal web server.
"""
logger = logging.getLogger(__name__)
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'software.cfg')), )
def getRootPartitionConnectionParameterDict(self):
"""Return the output paramters from the root partition"""
return json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
class TestPublishedURLIsReachableMixin(object): class TestPublishedURLIsReachableMixin(object):
...@@ -60,12 +47,14 @@ class TestPublishedURLIsReachableMixin(object): ...@@ -60,12 +47,14 @@ class TestPublishedURLIsReachableMixin(object):
# What happens is that instanciation just create the services, but does not # What happens is that instanciation just create the services, but does not
# wait for ERP5 to be initialized. When this test run ERP5 instance is # wait for ERP5 to be initialized. When this test run ERP5 instance is
# instanciated, but zope is still busy creating the site and haproxy replies # instanciated, but zope is still busy creating the site and haproxy replies
# with 503 Service Unavailable. # with 503 Service Unavailable, sometimes the first request is 404, so we
# retry in a loop.
# If we can move the "create site" in slapos node instance, then this retry loop # If we can move the "create site" in slapos node instance, then this retry loop
# would not be necessary. # would not be necessary.
for i in range(1, 20): for i in range(1, 60):
r = requests.get(url, verify=False) # XXX can we get CA from caucase already ? r = requests.get(url, verify=False) # XXX can we get CA from caucase already ?
if r.status_code == requests.codes.service_unavailable: if r.status_code in (requests.codes.service_unavailable,
requests.codes.not_found):
delay = i * 2 delay = i * 2
self.logger.warn("ERP5 was not available, sleeping for %ds and retrying", delay) self.logger.warn("ERP5 was not available, sleeping for %ds and retrying", delay)
time.sleep(delay) time.sleep(delay)
...@@ -91,13 +80,13 @@ class TestPublishedURLIsReachableMixin(object): ...@@ -91,13 +80,13 @@ class TestPublishedURLIsReachableMixin(object):
urlparse.urljoin(param_dict['family-default'], param_dict['site-id'])) urlparse.urljoin(param_dict['family-default'], param_dict['site-id']))
class TestDefaultParameters(ERP5TestCase, TestPublishedURLIsReachableMixin): class TestDefaultParameters(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 can be instanciated with no parameters """Test ERP5 can be instanciated with no parameters
""" """
__partition_reference__ = 'defp' __partition_reference__ = 'defp'
class TestMedusa(ERP5TestCase, TestPublishedURLIsReachableMixin): class TestMedusa(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 Medusa server """Test ERP5 Medusa server
""" """
__partition_reference__ = 'medusa' __partition_reference__ = 'medusa'
...@@ -107,7 +96,7 @@ class TestMedusa(ERP5TestCase, TestPublishedURLIsReachableMixin): ...@@ -107,7 +96,7 @@ class TestMedusa(ERP5TestCase, TestPublishedURLIsReachableMixin):
return {'_': json.dumps({'wsgi': False})} return {'_': json.dumps({'wsgi': False})}
class TestApacheBalancerPorts(ERP5TestCase): class TestApacheBalancerPorts(ERP5InstanceTestCase):
"""Instanciate with two zope families, this should create for each family: """Instanciate with two zope families, this should create for each family:
- a balancer entry point with corresponding haproxy - a balancer entry point with corresponding haproxy
- a balancer entry point for test runner - a balancer entry point for test runner
...@@ -159,8 +148,8 @@ class TestApacheBalancerPorts(ERP5TestCase): ...@@ -159,8 +148,8 @@ class TestApacheBalancerPorts(ERP5TestCase):
def test_zope_listen(self): def test_zope_listen(self):
# we requested 3 zope in family1 and 5 zopes in family2, we should have 8 zope running. # we requested 3 zope in family1 and 5 zopes in family2, we should have 8 zope running.
all_process_info = self.getSupervisorRPCServer( with self.slap.instance_supervisor_rpc as supervisor:
).supervisor.getAllProcessInfo() all_process_info = supervisor.getAllProcessInfo()
self.assertEqual( self.assertEqual(
3 + 5, 3 + 5,
len([p for p in all_process_info if p['name'].startswith('zope-')])) len([p for p in all_process_info if p['name'].startswith('zope-')]))
...@@ -168,8 +157,8 @@ class TestApacheBalancerPorts(ERP5TestCase): ...@@ -168,8 +157,8 @@ class TestApacheBalancerPorts(ERP5TestCase):
def test_apache_listen(self): def test_apache_listen(self):
# We have 2 families, apache should listen to a total of 3 ports per family # We have 2 families, apache should listen to a total of 3 ports per family
# normal access on ipv4 and ipv6 and test runner access on ipv4 only # normal access on ipv4 and ipv6 and test runner access on ipv4 only
all_process_info = self.getSupervisorRPCServer( with self.slap.instance_supervisor_rpc as supervisor:
).supervisor.getAllProcessInfo() all_process_info = supervisor.getAllProcessInfo()
process_info, = [p for p in all_process_info if p['name'] == 'apache'] process_info, = [p for p in all_process_info if p['name'] == 'apache']
apache_process = psutil.Process(process_info['pid']) apache_process = psutil.Process(process_info['pid'])
self.assertEqual( self.assertEqual(
...@@ -182,8 +171,8 @@ class TestApacheBalancerPorts(ERP5TestCase): ...@@ -182,8 +171,8 @@ class TestApacheBalancerPorts(ERP5TestCase):
def test_haproxy_listen(self): def test_haproxy_listen(self):
# There is one haproxy per family # There is one haproxy per family
all_process_info = self.getSupervisorRPCServer( with self.slap.instance_supervisor_rpc as supervisor:
).supervisor.getAllProcessInfo() all_process_info = supervisor.getAllProcessInfo()
process_info, = [ process_info, = [
p for p in all_process_info if p['name'].startswith('haproxy-') p for p in all_process_info if p['name'].startswith('haproxy-')
] ]
...@@ -193,7 +182,7 @@ class TestApacheBalancerPorts(ERP5TestCase): ...@@ -193,7 +182,7 @@ class TestApacheBalancerPorts(ERP5TestCase):
]) ])
class TestDisableTestRunner(ERP5TestCase, TestPublishedURLIsReachableMixin): class TestDisableTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 can be instanciated without test runner. """Test ERP5 can be instanciated without test runner.
""" """
__partition_reference__ = 'distr' __partition_reference__ = 'distr'
...@@ -215,8 +204,8 @@ class TestDisableTestRunner(ERP5TestCase, TestPublishedURLIsReachableMixin): ...@@ -215,8 +204,8 @@ class TestDisableTestRunner(ERP5TestCase, TestPublishedURLIsReachableMixin):
def test_no_apache_testrunner_port(self): def test_no_apache_testrunner_port(self):
# Apache only listen on two ports, there is no apache ports allocated for test runner # Apache only listen on two ports, there is no apache ports allocated for test runner
all_process_info = self.getSupervisorRPCServer( with self.slap.instance_supervisor_rpc as supervisor:
).supervisor.getAllProcessInfo() all_process_info = supervisor.getAllProcessInfo()
process_info, = [p for p in all_process_info if p['name'] == 'apache'] process_info, = [p for p in all_process_info if p['name'] == 'apache']
apache_process = psutil.Process(process_info['pid']) apache_process = psutil.Process(process_info['pid'])
self.assertEqual( self.assertEqual(
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the `slapos.core.XXX` computer partition instance.
* `computer_partition_root_path`: the path of the instance root directory,
A note about paths:
SlapOS itself and some services running in SlapOS uses unix sockets and (sometimes very)
deep path, which does not play very well together. To workaround this, users can
set SLAPOS_TEST_WORKING_DIR enivonment variable to the path of a short enough directory
and local slapos will be in this directory.
The partitions references will be named after the unittest class name, which can also lead
to long paths. For this, unit test classes can define a __partition_reference__ attribute
which will be used as partition reference. The trick is then to use a shorter
__partition_reference__
See https://lab.nexedi.com/kirr/slapns for the solution to all these problems.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except BaseException:
cls.stopSlapOSProcesses()
cls.setUp = lambda self: self.fail('Setup Class failed.')
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': getattr(cls, '__partition_reference__', cls.__name__),
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -28,23 +28,25 @@ from setuptools import setup, find_packages ...@@ -28,23 +28,25 @@ from setuptools import setup, find_packages
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.helloworld' name = 'slapos.test.helloworld'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(
version=version, name=name,
description="Test for SlapOS' helloworld", version=version,
long_description=long_description, description="Test for SlapOS' helloworld",
long_description_content_type='text/markdown', long_description=long_description,
maintainer="Nexedi", long_description_content_type='text/markdown',
maintainer_email="info@nexedi.com", maintainer="Nexedi",
url="https://lab.nexedi.com/nexedi/slapos", maintainer_email="info@nexedi.com",
packages=find_packages(), url="https://lab.nexedi.com/nexedi/slapos",
install_requires=[ packages=find_packages(),
install_requires=[
'slapos.core', 'slapos.core',
'slapos.libnetworkcache', 'slapos.libnetworkcache',
'erp5.util', 'erp5.util',
'requests', 'requests',
], ],
zip_safe=True, zip_safe=True,
test_suite='test', test_suite='test',
) )
...@@ -27,26 +27,19 @@ ...@@ -27,26 +27,19 @@
import os import os
import requests import requests
import utils
# for development: debugging logs and install Ctrl+C handler from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class HelloWorldTestCase(utils.SlapOSInstanceTestCase):
class HelloWorldTestCase(SlapOSInstanceTestCase):
# to be defined by subclasses # to be defined by subclasses
name = None name = None
kind = None kind = None
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')),)
@classmethod @classmethod
def getInstanceParameterDict(cls): def getInstanceParameterDict(cls):
return {"name": cls.name} return {"name": cls.name}
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -25,26 +25,20 @@ ...@@ -25,26 +25,20 @@
# #
############################################################################## ##############################################################################
import utils
import httplib import httplib
import json import json
import os import os
import requests import requests
# for development: debugging logs and install Ctrl+C handler from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, InstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class TestJupyter(utils.SlapOSInstanceTestCase):
@classmethod class TestJupyter(InstanceTestCase):
def getSoftwareURLList(cls):
return (os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')),)
def test(self): def test(self):
parameter_dict = self.computer_partition.getConnectionParameterDict() parameter_dict = self.computer_partition.getConnectionParameterDict()
...@@ -54,12 +48,11 @@ class TestJupyter(utils.SlapOSInstanceTestCase): ...@@ -54,12 +48,11 @@ class TestJupyter(utils.SlapOSInstanceTestCase):
except Exception as e: except Exception as e:
self.fail("Can't parse json in %s, error %s" % (parameter_dict['_'], e)) self.fail("Can't parse json in %s, error %s" % (parameter_dict['_'], e))
ip = os.environ['SLAPOS_TEST_IPV6']
self.assertEqual( self.assertEqual(
{ {
'jupyter-classic-url': 'https://[%s]:8888/tree' % (ip,), 'jupyter-classic-url': 'https://[%s]:8888/tree' % (self._ipv6_address, ),
'jupyterlab-url': 'https://[%s]:8888/lab' % (ip,), 'jupyterlab-url': 'https://[%s]:8888/lab' % (self._ipv6_address, ),
'url': 'https://[%s]:8888/tree' % (ip,) 'url': 'https://[%s]:8888/tree' % (self._ipv6_address, )
}, },
connection_dict connection_dict
) )
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during
`setUpClass` and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be
defined on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': 'T', # minimise path length, see https://github.com/apache/trafficserver/issues/2421
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls, max_quantity=None):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
run_cp_kw = {}
if max_quantity is not None:
run_cp_kw['max_quantity'] = max_quantity
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ,
**run_cp_kw)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -4,8 +4,7 @@ kvm ...@@ -4,8 +4,7 @@ kvm
Introduction Introduction
------------ ------------
This software release is used to deploy KVM instances, NBD instances and This software release is used to deploy KVM and NBD instances.
Frontend instances of KVM.
For extensive parameters definition, please look at parameter-input-schema.json. For extensive parameters definition, please look at parameter-input-schema.json.
...@@ -35,9 +34,10 @@ See the instance-kvm-input-schema.json file for more instance parameters (cpu-co ...@@ -35,9 +34,10 @@ See the instance-kvm-input-schema.json file for more instance parameters (cpu-co
KVM instance parameters: KVM instance parameters:
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
- frontend-software-type (default: frontend) - frontend-software-type (default: RootSoftwareInstance)
- frontend-software-url (default: https://lab.nexedi.com/nexedi/slapos/raw/slapos-0.92/software/kvm/software.cfg) - frontend-software-url (default: http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg)
- frontend-instance-guid - frontend-instance-guid
- frontend-addtional-instance-guid
- frontend-instance-name (default: VNC Frontend) - frontend-instance-name (default: VNC Frontend)
- nbd-port (default: 1024) - nbd-port (default: 1024)
- nbd-host - nbd-host
...@@ -119,21 +119,3 @@ files for more instance parameters (cpu-count, ram-size, disk-size, specific loc ...@@ -119,21 +119,3 @@ files for more instance parameters (cpu-count, ram-size, disk-size, specific loc
Then, if you want one of the two clones to takeover, you need to login into Then, if you want one of the two clones to takeover, you need to login into
the hosting machine, go to the partition of the clone, and invoke bin/takeover. the hosting machine, go to the partition of the clone, and invoke bin/takeover.
KVM Frontend Master Instance (will host all frontend Slave Instances)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This type of instance will allow to host any frontend slave instance requested
by KVM instances. Slave instances (and thus KVM instance) will be accessible
at : https://mydomain.com/instancereference .
::
mykvmfrontend = request(
software_release=kvm,
partition_reference="mykvmfrontend",
partition_parameter_kw={
"domain":"mydomain.com"
},
software_type="frontend",
)
...@@ -15,15 +15,15 @@ ...@@ -15,15 +15,15 @@
[template] [template]
filename = instance.cfg.in filename = instance.cfg.in
md5sum = 028b6a6456d744c11b1bb2c51ecd51b2 md5sum = 2cbfd6b08c65369c1d45cf3ba2ff335a
[template-kvm] [template-kvm]
filename = instance-kvm.cfg.jinja2 filename = instance-kvm.cfg.jinja2
md5sum = 325326fa7266757dfed028b11aae58a0 md5sum = dc4dc40f9d950b5f963c22c62ca488a7
[template-kvm-cluster] [template-kvm-cluster]
filename = instance-kvm-cluster.cfg.jinja2.in filename = instance-kvm-cluster.cfg.jinja2.in
md5sum = 2bbee46d39aec87e92c8462efab292b6 md5sum = c7ce0510c05753d428c18b756f827573
[template-kvm-resilient] [template-kvm-resilient]
filename = instance-kvm-resilient.cfg.jinja2 filename = instance-kvm-resilient.cfg.jinja2
...@@ -49,10 +49,6 @@ md5sum = b617d64de73de1eed518185f310bbc82 ...@@ -49,10 +49,6 @@ md5sum = b617d64de73de1eed518185f310bbc82
filename = instance-nbd.cfg.in filename = instance-nbd.cfg.in
md5sum = f634a5249b773658b7a7bc9fa9bb0368 md5sum = f634a5249b773658b7a7bc9fa9bb0368
[template-frontend]
filename = instance-frontend.cfg.in
md5sum = 57a4be74e5afb00e378bc726cd7771f8
[template-ansible-promise] [template-ansible-promise]
filename = template/ansible-promise.in filename = template/ansible-promise.in
md5sum = 2036bf145f472f62ef8dee5e729328fd md5sum = 2036bf145f472f62ef8dee5e729328fd
......
#############################
#
# Instanciate kvm frontend
#
#############################
[buildout]
parts =
ca-frontend
certificate-authority
frontend-promise-ipv6
frontend-promise-ipv4
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[rootdirectory]
recipe = slapos.cookbook:mkdirectory
etc = $${buildout:directory}/etc
bin = $${buildout:directory}/bin
srv = $${buildout:directory}/srv
var = $${buildout:directory}/var
[basedirectory]
recipe = slapos.cookbook:mkdirectory
services = $${rootdirectory:etc}/run
promises = $${rootdirectory:etc}/promise
nodejs-conf = $${rootdirectory:etc}/nodejs
run = $${rootdirectory:var}/run
log = $${rootdirectory:var}/log
ca-dir = $${rootdirectory:srv}/ssl
backup = $${rootdirectory:srv}/backup
[directory]
recipe = slapos.cookbook:mkdirectory
ca-dir = $${rootdirectory:srv}/ssl
[frontend-instance]
recipe = slapos.cookbook:kvm.frontend
domain = $${ca-frontend:name}
# port = $${slap-parameter:port}
ipv6 = $${slap-network-information:global-ipv6}
ipv4 = $${slap-network-information:local-ipv4}
port = $${slap-parameter:port}
http-redirection = $${slap-parameter:http-redirection}
ssl-key-path = $${ca-frontend:key-file}
ssl-cert-path = $${ca-frontend:cert-file}
slave-instance-list = $${slap-parameter:slave_instance_list}
map-path = $${basedirectory:nodejs-conf}/proxy_table.json
conf-path = $${basedirectory:nodejs-conf}/kvm-proxy.js
wrapper-path = $${rootdirectory:bin}/kvm_frontend
node-binary = ${nodejs:location}/bin/node
node-env = ${buildout:parts-directory}:${npm-modules:location}/node_modules
shell-path = ${dash:location}/bin/dash
[frontend-promise-ipv6]
recipe = slapos.cookbook:check_port_listening
path = $${basedirectory:promises}/frontend_promise
hostname = $${frontend-instance:ipv6}
port = $${frontend-instance:port}
[frontend-promise-ipv4]
recipe = slapos.cookbook:check_port_listening
path = $${basedirectory:promises}/frontend_promise
hostname = $${frontend-instance:ipv4}
port = $${frontend-instance:port}
[certificate-authority]
recipe = slapos.cookbook:certificate_authority
openssl-binary = ${openssl:location}/bin/openssl
ca-dir = $${basedirectory:ca-dir}
requests-directory = $${cadirectory:requests}
wrapper = $${basedirectory:services}/certificate_authority
ca-private = $${cadirectory:private}
ca-certs = $${cadirectory:certs}
ca-newcerts = $${cadirectory:newcerts}
ca-crl = $${cadirectory:crl}
[cadirectory]
recipe = slapos.cookbook:mkdirectory
requests = $${basedirectory:ca-dir}/requests/
private = $${basedirectory:ca-dir}/private/
certs = $${basedirectory:ca-dir}/certs/
newcerts = $${basedirectory:ca-dir}/newcerts/
crl = $${basedirectory:ca-dir}/crl/
[ca-frontend]
<= certificate-authority
recipe = slapos.cookbook:certificate_authority.request
key-file = $${basedirectory:nodejs-conf}/nodejs.key
cert-file = $${basedirectory:nodejs-conf}/nodejs.crt
executable = $${frontend-instance:wrapper-path}
wrapper = $${basedirectory:services}/nodejs
# Put domain name
name = $${slap-parameter:domain}
[slap-parameter]
# Default value if no port is specified
port = 4443
http-redirection = 0
slave_instance_list =
...@@ -10,21 +10,38 @@ ...@@ -10,21 +10,38 @@
"frontend-instance-guid": { "frontend-instance-guid": {
"title": "Frontend Instance ID", "title": "Frontend Instance ID",
"description": "Unique identifier of the frontend instance, like \"SOFTINST-11031\".", "description": "Unique identifier of the frontend instance, like \"SOFTINST-11031\".",
"type": "string", "type": "string"
"default": ""
}, },
"frontend-software-type": { "frontend-software-type": {
"title": "Frontend Software Type", "title": "Frontend Software Type",
"description": "Type of the frontend instance, like \"frontend\".", "description": "Type of the frontend instance, like \"RootSoftwareInstance\".",
"type": "string", "type": "string",
"default": "frontend" "default": "RootSoftwareInstance"
}, },
"frontend-software-url": { "frontend-software-url": {
"title": "Frontend Software URL", "title": "Frontend Software URL",
"description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".", "description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".",
"type": "string", "type": "string",
"format": "uri", "format": "uri",
"default": "https://lab.nexedi.com/nexedi/slapos/raw/slapos-0.92/software/kvm/software.cfg" "default": "http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg"
},
"frontend-additional-instance-guid": {
"title": "Additional Frontend Instance ID",
"description": "Unique identifier of the additional frontend instance, like \"SOFTINST-11031\", if empty won't be requested.",
"type": "string"
},
"frontend-additional-software-type": {
"title": "Additional Frontend Software Type",
"description": "Type of the frontend instance, like \"RootSoftwareInstance\".",
"type": "string",
"default": "RootSoftwareInstance"
},
"frontend-additional-software-url": {
"title": "Additional Frontend Software URL",
"description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".",
"type": "string",
"format": "uri",
"default": "http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg"
} }
}, },
"type": "object" "type": "object"
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
{% set slave_frontend_sr = slave_frontend_dict.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg') -%} {% set slave_frontend_sr = slave_frontend_dict.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg') -%}
{% set slave_frontend_stype = slave_frontend_dict.get('software-type', 'custom-personal') -%} {% set slave_frontend_stype = slave_frontend_dict.get('software-type', 'custom-personal') -%}
{% set slave_frontend_iguid = slave_frontend_dict.get('instance-guid', '') -%} {% set slave_frontend_iguid = slave_frontend_dict.get('instance-guid', '') -%}
{% set WEBSOCKET_FRONTEND_DEFAULT_SR = 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg' %}
{% set WEBSOCKET_FRONTEND_DEFAULT_ST = 'RootSoftwareInstance' %}
{% set kvm_instance_dict = {} -%} {% set kvm_instance_dict = {} -%}
{% set kvm_hostname_list = [] -%} {% set kvm_hostname_list = [] -%}
{% set monitor_base_url_dict = {} -%} {% set monitor_base_url_dict = {} -%}
...@@ -47,10 +49,16 @@ sla-project_guid = {{ dumps(kvm_parameter_dict.get('project-guid', '')) }} ...@@ -47,10 +49,16 @@ sla-project_guid = {{ dumps(kvm_parameter_dict.get('project-guid', '')) }}
state = stopped state = stopped
{% endif -%} {% endif -%}
config-frontend-instance-name = {{ instance_name ~ ' VNC Frontend' }} config-frontend-instance-name = {{ instance_name ~ ' VNC Real Frontend' }}
config-frontend-software-type = {{ dumps(frontend_dict.get('software-type', 'frontend')) }} {{ setconfig('frontend-software-url', frontend_dict.get('frontend-software-url', WEBSOCKET_FRONTEND_DEFAULT_SR)) }}
config-frontend-software-url = {{ dumps(frontend_dict.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/refs/tags/slapos-0.92:/software/kvm/software.cfg')) }} {{ setconfig('frontend-software-type', frontend_dict.get('frontend-software-type', WEBSOCKET_FRONTEND_DEFAULT_ST)) }}
{{ setconfig('frontend-instance-guid', kvm_parameter_dict.get('instance-guid', '')) }} {{ setconfig('frontend-instance-guid', frontend_dict.get('frontend-instance-guid', '')) }}
config-frontend-additional-instance-name = {{ instance_name ~ ' VNC Real Frontend Additional' }}
{{ setconfig('frontend-additional-software-url', frontend_dict.get('frontend-additional-software-url', WEBSOCKET_FRONTEND_DEFAULT_SR)) }}
{{ setconfig('frontend-additional-software-type', frontend_dict.get('frontend-additional-software-type', WEBSOCKET_FRONTEND_DEFAULT_ST)) }}
{{ setconfig('frontend-additional-instance-guid', frontend_dict.get('frontend-additional-instance-guid', '')) }}
config-name = {{ instance_name }} config-name = {{ instance_name }}
{% if slapparameter_dict.get('authorized-keys', []) -%} {% if slapparameter_dict.get('authorized-keys', []) -%}
config-authorized-key = {{ dumps(slapparameter_dict.get('authorized-keys') | join('\n')) }} config-authorized-key = {{ dumps(slapparameter_dict.get('authorized-keys') | join('\n')) }}
...@@ -100,7 +108,7 @@ config-httpd-port = {{ dumps(kvm_parameter_dict.get('httpd-port', 8081)) }} ...@@ -100,7 +108,7 @@ config-httpd-port = {{ dumps(kvm_parameter_dict.get('httpd-port', 8081)) }}
config-disable-ansible-promise = {{ dumps(kvm_parameter_dict.get('disable-ansible-promise', False)) }} config-disable-ansible-promise = {{ dumps(kvm_parameter_dict.get('disable-ansible-promise', False)) }}
config-monitor-cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.app.officejs.com') }} config-monitor-cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.app.officejs.com') }}
config-monitor-username = ${monitor-instance-parameter:username} config-monitor-username = ${monitor-instance-parameter:username}
config-monitor-password = ${publish-early:monitor-password} config-monitor-password = ${monitor-htpasswd:passwd}
# Enable disk wipe options # Enable disk wipe options
{% if kvm_parameter_dict.get('wipe-disk-ondestroy', False) -%} {% if kvm_parameter_dict.get('wipe-disk-ondestroy', False) -%}
config-wipe-disk-ondestroy = True config-wipe-disk-ondestroy = True
...@@ -126,6 +134,9 @@ sla-fw_restricted_access = {{ dumps(slapparameter_dict.get('fw-restricted-access ...@@ -126,6 +134,9 @@ sla-fw_restricted_access = {{ dumps(slapparameter_dict.get('fw-restricted-access
return = return =
url url
{% if frontend_dict.get('frontend-additional-instance-guid', '') %}
url-additional
{% endif %}
backend-url backend-url
{% if str(use_nat).lower() == 'true' -%} {% if str(use_nat).lower() == 'true' -%}
{% for port in nat_rules_list -%} {% for port in nat_rules_list -%}
...@@ -151,6 +162,9 @@ return = ...@@ -151,6 +162,9 @@ return =
{% do monitor_base_url_dict.__setitem__(instance_name, '${' ~ section ~ ':connection-monitor-base-url}') -%} {% do monitor_base_url_dict.__setitem__(instance_name, '${' ~ section ~ ':connection-monitor-base-url}') -%}
{% do publish_dict.__setitem__(instance_name ~ '-backend-url', '${' ~ section ~ ':connection-backend-url}') -%} {% do publish_dict.__setitem__(instance_name ~ '-backend-url', '${' ~ section ~ ':connection-backend-url}') -%}
{% do publish_dict.__setitem__(instance_name ~ '-url', '${' ~ section ~ ':connection-url}') -%} {% do publish_dict.__setitem__(instance_name ~ '-url', '${' ~ section ~ ':connection-url}') -%}
{% if frontend_dict.get('frontend-additional-instance-guid', '') %}
{% do publish_dict.__setitem__(instance_name ~ '-url-additional', '${' ~ section ~ ':connection-url-additional}') -%}
{% endif %}
{% do kvm_instance_dict.__setitem__(instance_name, (use_nat, nat_rules_list)) -%} {% do kvm_instance_dict.__setitem__(instance_name, (use_nat, nat_rules_list)) -%}
{% endfor %} {% endfor %}
...@@ -256,11 +270,6 @@ mode = {{ mode }} ...@@ -256,11 +270,6 @@ mode = {{ mode }}
{{ writefile('cluster-data-content', '${directory:webroot}/${hash-code:passwd}/data', slapparameter_dict.get('cluster-data', ''), '700') }} {{ writefile('cluster-data-content', '${directory:webroot}/${hash-code:passwd}/data', slapparameter_dict.get('cluster-data', ''), '700') }}
{% endif -%} {% endif -%}
[publish-early]
recipe = slapos.cookbook:publish-early
-init =
monitor-password monitor-htpasswd:passwd
[monitor-instance-parameter] [monitor-instance-parameter]
monitor-httpd-port = 8060 monitor-httpd-port = 8060
cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.app.officejs.com') }} cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.app.officejs.com') }}
...@@ -277,14 +286,11 @@ private-path-list += ...@@ -277,14 +286,11 @@ private-path-list +=
${directory:webroot}/ ${directory:webroot}/
[publish-connection-information] [publish-connection-information]
<= monitor-publish
recipe = slapos.cookbook:publish recipe = slapos.cookbook:publish
{% for name, value in publish_dict.items() -%} {% for name, value in publish_dict.items() -%}
{{ name }} = {{ value }} {{ name }} = {{ value }}
{% endfor %} {% endfor %}
{% set monitor_interface_url = slapparameter_dict.get('monitor-interface-url', 'https://monitor.app.officejs.com') -%}
monitor-setup-url = {{ monitor_interface_url }}/#page=settings_configurator&url=${monitor-publish-parameters:monitor-url}&username=${monitor-publish-parameters:monitor-user}&password=${monitor-publish-parameters:monitor-password}
monitor-password = ${publish-early:monitor-password}
monitor-user = ${monitor-publish-parameters:monitor-user}
{% do part_list.append('monitor-base') -%} {% do part_list.append('monitor-base') -%}
[buildout] [buildout]
......
...@@ -336,21 +336,38 @@ ...@@ -336,21 +336,38 @@
"frontend-instance-guid": { "frontend-instance-guid": {
"title": "Frontend Instance ID", "title": "Frontend Instance ID",
"description": "Unique identifier of the frontend instance, like \"SOFTINST-11031\".", "description": "Unique identifier of the frontend instance, like \"SOFTINST-11031\".",
"type": "string", "type": "string"
"default": "SOFTINST-11031"
}, },
"frontend-software-type": { "frontend-software-type": {
"title": "Frontend Software Type", "title": "Frontend Software Type",
"description": "Type of the frontend instance, like \"frontend\".", "description": "Type of the frontend instance, like \"frontend\".",
"type": "string", "type": "string",
"default": "frontend" "default": "RootSoftwareInstance"
}, },
"frontend-software-url": { "frontend-software-url": {
"title": "Frontend Software URL", "title": "Frontend Software URL",
"description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".", "description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".",
"type": "string", "type": "string",
"format": "uri", "format": "uri",
"default": "https://lab.nexedi.com/nexedi/slapos/raw/slapos-0.92/software/kvm/software.cfg" "default": "http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg"
},
"frontend-additional-instance-guid": {
"title": "Additional Frontend Instance ID",
"description": "Unique identifier of the additional frontend instance, like \"SOFTINST-11031\", if empty won't be requested.",
"type": "string"
},
"frontend-additional-software-type": {
"title": "Additional Frontend Software Type",
"description": "Type of the frontend instance, like \"frontend\".",
"type": "string",
"default": "RootSoftwareInstance"
},
"frontend-additional-software-url": {
"title": "Additional Frontend Software URL",
"description": "Software Release URL of the frontend instance, like \"http://example.com/path/to/software.cfg\".",
"type": "string",
"format": "uri",
"default": "http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg"
} }
} }
} }
{% set additional_frontend = (slapparameter_dict.get('frontend-additional-instance-guid', '').strip() != '') %}
{% set enable_http = slapparameter_dict.get('enable-http-server', 'False').lower() -%} {% set enable_http = slapparameter_dict.get('enable-http-server', 'False').lower() -%}
{% set use_tap = slapparameter_dict.get('use-tap', 'True').lower() -%} {% set use_tap = slapparameter_dict.get('use-tap', 'True').lower() -%}
{% set use_nat = slapparameter_dict.get('use-nat', 'True').lower() -%} {% set use_nat = slapparameter_dict.get('use-nat', 'True').lower() -%}
...@@ -8,7 +9,6 @@ ...@@ -8,7 +9,6 @@
{% set enable_device_hotplug = slapparameter_dict.get('enable-device-hotplug', 'false').lower() -%} {% set enable_device_hotplug = slapparameter_dict.get('enable-device-hotplug', 'false').lower() -%}
{% set instance_type = slapparameter_dict.get('type', 'standalone') -%} {% set instance_type = slapparameter_dict.get('type', 'standalone') -%}
{% set nat_rule_list = slapparameter_dict.get('nat-rules', '22 80 443') -%} {% set nat_rule_list = slapparameter_dict.get('nat-rules', '22 80 443') -%}
{% set frontend_software_type = 'default' -%}
{% set disk_device_path = slapparameter_dict.get('disk-device-path', None) -%} {% set disk_device_path = slapparameter_dict.get('disk-device-path', None) -%}
{% set extends_list = [] -%} {% set extends_list = [] -%}
{% set part_list = [] -%} {% set part_list = [] -%}
...@@ -359,20 +359,24 @@ log = ${directory:log}/crond.log ...@@ -359,20 +359,24 @@ log = ${directory:log}/crond.log
#-- #--
#-- Deploy frontend. #-- Deploy frontend.
[request-slave-frontend] [request-slave-frontend-base]
recipe = slapos.cookbook:requestoptional recipe = slapos.cookbook:requestoptional
software-url = ${slap-parameter:frontend-software-url}
server-url = ${slap-connection:server-url} server-url = ${slap-connection:server-url}
key-file = ${slap-connection:key-file} key-file = ${slap-connection:key-file}
cert-file = ${slap-connection:cert-file} cert-file = ${slap-connection:cert-file}
computer-id = ${slap-connection:computer-id} computer-id = ${slap-connection:computer-id}
partition-id = ${slap-connection:partition-id} partition-id = ${slap-connection:partition-id}
name = ${slap-parameter:frontend-instance-name}
software-type = ${slap-parameter:frontend-software-type}
slave = true slave = true
config-host = ${novnc-instance:ip} config-https-only = True
config-port = ${novnc-instance:port} config-type = websocket
return = url resource port domainname config-url = https://[${novnc-instance:ip}]:${novnc-instance:port}
return = secure_access domain
[request-slave-frontend]
<= request-slave-frontend-base
software-url = ${slap-parameter:frontend-software-url}
software-type = ${slap-parameter:frontend-software-type}
name = ${slap-parameter:frontend-instance-name}
sla-instance_guid = ${slap-parameter:frontend-instance-guid} sla-instance_guid = ${slap-parameter:frontend-instance-guid}
[frontend-promise] [frontend-promise]
...@@ -382,6 +386,21 @@ url = ${publish-connection-information:url} ...@@ -382,6 +386,21 @@ url = ${publish-connection-information:url}
dash_path = {{ dash_executable_location }} dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }} curl_path = {{ curl_executable_location }}
{% if additional_frontend %}
[request-slave-frontend-additional]
<= request-slave-frontend-base
software-url = ${slap-parameter:frontend-additional-software-url}
software-type = ${slap-parameter:frontend-additional-software-type}
name = ${slap-parameter:frontend-additional-instance-name}
sla-instance_guid = ${slap-parameter:frontend-additional-instance-guid}
[frontend-additional-promise]
recipe = slapos.cookbook:check_url_available
path = ${directory:promises}/frontend_additional_promise
url = ${publish-connection-information:url-additional}
dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }}
{% endif %}
{% if enable_http == 'true' %} {% if enable_http == 'true' %}
[httpd] [httpd]
...@@ -422,12 +441,47 @@ interface-url = {{ slapparameter_dict.get('monitor-interface-url', 'https://moni ...@@ -422,12 +441,47 @@ interface-url = {{ slapparameter_dict.get('monitor-interface-url', 'https://moni
[helper] [helper]
blank-line = blank-line =
[frontend-port-execute-base]
recipe = plone.recipe.command
command =
set -e
port=$(echo '${request-slave-frontend:connection-secure_access}' | cut -d ':' -f 3 | cut -d '/' -f 1)
[ -z $port ] && port=443
echo $port > ${:output}
update-command = ${:command}
stop-on-error = True
[frontend-port-execute]
<= frontend-port-execute-base
secure_access = ${request-slave-frontend:connection-secure_access}
output = ${directory:var}/frontend_port.txt
[frontend-port]
recipe = collective.recipe.shelloutput
filename = ${frontend-port-execute:output}
commands =
port = [ -f '${:filename}' ] && cat '${:filename}' || echo "NotReady"
{% if additional_frontend %}
[frontend-additional-port-execute]
<= frontend-port-execute-base
secure_access = ${request-slave-frontend-additional:connection-secure_access}
output = ${directory:var}/frontend_additional_port.txt
[frontend-additional-port]
<= frontend-port
filename = ${frontend-additional-port-execute:output}
{% endif %}
[publish-connection-information] [publish-connection-information]
<= monitor-publish <= monitor-publish
recipe = slapos.cookbook:publish recipe = slapos.cookbook:publish
ipv6 = ${slap-network-information:global-ipv6} ipv6 = ${slap-network-information:global-ipv6}
backend-url = https://[${novnc-instance:ip}]:${novnc-instance:port}/vnc.html?host=[${novnc-instance:ip}]&port=${novnc-instance:port}&encrypt=1&password=${kvm-controller-parameter-dict:vnc-passwd} backend-url = https://[${novnc-instance:ip}]:${novnc-instance:port}/vnc.html?host=[${novnc-instance:ip}]&port=${novnc-instance:port}&encrypt=1&password=${kvm-controller-parameter-dict:vnc-passwd}
url = ${request-slave-frontend:connection-url}/vnc.html?host=${request-slave-frontend:connection-domainname}&port=${request-slave-frontend:connection-port}&encrypt=1&path=${request-slave-frontend:connection-resource}&password=${kvm-controller-parameter-dict:vnc-passwd} url = ${request-slave-frontend:connection-secure_access}/vnc.html?host=${request-slave-frontend:connection-domain}&port=${frontend-port:port}&encrypt=1&password=${kvm-controller-parameter-dict:vnc-passwd}
{% if additional_frontend %}
url-additional = ${request-slave-frontend-additional:connection-secure_access}/vnc.html?host=${request-slave-frontend-additional:connection-domain}&port=${frontend-additional-port:port}&encrypt=1&password=${kvm-controller-parameter-dict:vnc-passwd}
{% endif %}
{% set disk_number = len(storage_dict) -%} {% set disk_number = len(storage_dict) -%}
maximum-extra-disk-amount = {{ disk_number }} maximum-extra-disk-amount = {{ disk_number }}
{% set iface = 'ens3' -%} {% set iface = 'ens3' -%}
...@@ -648,10 +702,14 @@ log = ${directory:public}/ansible/vm-bootstrap.log ...@@ -648,10 +702,14 @@ log = ${directory:public}/ansible/vm-bootstrap.log
[slap-parameter] [slap-parameter]
# Default values if not specified # Default values if not specified
frontend-software-type = frontend frontend-software-type = RootSoftwareInstance
frontend-software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/refs/tags/slapos-0.92:/software/kvm/software.cfg frontend-software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
frontend-instance-guid = frontend-instance-guid =
frontend-instance-name = VNC Frontend frontend-instance-name = VNC Real Frontend
frontend-additional-software-type = RootSoftwareInstance
frontend-additional-software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
frontend-additional-instance-guid =
frontend-additional-instance-name = VNC Real Frontend Additional
nbd-port = 1024 nbd-port = 1024
nbd-host = nbd-host =
nbd2-port = 1024 nbd2-port = 1024
...@@ -763,6 +821,9 @@ parts = ...@@ -763,6 +821,9 @@ parts =
cron-service cron-service
cron-entry-logrotate cron-entry-logrotate
frontend-promise frontend-promise
{% if additional_frontend %}
frontend-additional-promise
{% endif %}
# monitor parts # monitor parts
monitor-base monitor-base
# Complete parts with sections # Complete parts with sections
......
...@@ -13,7 +13,6 @@ default = $${:kvm} ...@@ -13,7 +13,6 @@ default = $${:kvm}
kvm-cluster = $${dynamic-template-kvm-cluster:rendered} kvm-cluster = $${dynamic-template-kvm-cluster:rendered}
kvm = $${dynamic-template-kvm:rendered} kvm = $${dynamic-template-kvm:rendered}
nbd = ${template-nbd:output} nbd = ${template-nbd:output}
frontend = ${template-frontend:output}
kvm-resilient = $${dynamic-template-kvm-resilient:rendered} kvm-resilient = $${dynamic-template-kvm-resilient:rendered}
kvm-import = $${dynamic-template-kvm-import:rendered} kvm-import = $${dynamic-template-kvm-import:rendered}
......
...@@ -56,6 +56,7 @@ eggs = ...@@ -56,6 +56,7 @@ eggs =
collective.recipe.template collective.recipe.template
plone.recipe.command plone.recipe.command
${pycurl:egg} ${pycurl:egg}
collective.recipe.shelloutput
[http-proxy] [http-proxy]
# https://github.com/nodejitsu/node-http-proxy # https://github.com/nodejitsu/node-http-proxy
...@@ -146,10 +147,6 @@ mode = 0755 ...@@ -146,10 +147,6 @@ mode = 0755
<= template-file-base <= template-file-base
output = ${buildout:directory}/template-nbd.cfg output = ${buildout:directory}/template-nbd.cfg
[template-frontend]
<= template-file-base
output = ${buildout:directory}/template-frontend.cfg
[template-ansible-promise] [template-ansible-promise]
<= download-template-base <= download-template-base
filename = ansible-promise.in filename = ansible-promise.in
...@@ -201,6 +198,7 @@ context = ...@@ -201,6 +198,7 @@ context =
websockify = 0.5.1 websockify = 0.5.1
collective.recipe.environment = 0.2.0 collective.recipe.environment = 0.2.0
collective.recipe.shelloutput = 0.1
gitdb = 0.6.4 gitdb = 0.6.4
pycurl = 7.43.0 pycurl = 7.43.0
slapos.recipe.template = 4.3 slapos.recipe.template = 4.3
......
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.kvm' name = 'slapos.test.kvm'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -25,31 +25,22 @@ ...@@ -25,31 +25,22 @@
# #
############################################################################## ##############################################################################
import httplib
import json
import os import os
import shutil
import urlparse
import tempfile
import requests import requests
import socket import slapos.util
import StringIO import sqlite3
import subprocess import urlparse
import json
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, InstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class InstanceTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class ServicesTestCase(InstanceTestCase): class ServicesTestCase(InstanceTestCase):
...@@ -67,9 +58,9 @@ class ServicesTestCase(InstanceTestCase): ...@@ -67,9 +58,9 @@ class ServicesTestCase(InstanceTestCase):
'websockify-{hash}-on-watch', 'websockify-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [os.path.join(self.computer_partition_root_path, path)
for path in hash_files] for path in hash_files]
...@@ -79,3 +70,185 @@ class ServicesTestCase(InstanceTestCase): ...@@ -79,3 +70,185 @@ class ServicesTestCase(InstanceTestCase):
expected_process_name = name.format(hash=h) expected_process_name = name.format(hash=h)
self.assertIn(expected_process_name, process_names) self.assertIn(expected_process_name, process_names)
class MonitorAccessMixin(object):
def sqlite3_connect(self):
sqlitedb_file = os.path.join(
os.path.abspath(
os.path.join(
self.slap.instance_directory, os.pardir
)
), 'var', 'proxy.db'
)
return sqlite3.connect(sqlitedb_file)
def get_all_instantiated_partition_list(self):
connection = self.sqlite3_connect()
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
connection.row_factory = dict_factory
cursor = connection.cursor()
cursor.execute(
"SELECT reference, xml, connection_xml, partition_reference, "
"software_release, requested_state, software_type "
"FROM partition14 "
"WHERE slap_state='busy'")
return cursor.fetchall()
def test_access_monitor(self):
connection_parameter_dict = self.computer_partition\
.getConnectionParameterDict()
monitor_setup_url = connection_parameter_dict['monitor-setup-url']
monitor_url_with_auth = 'https' + monitor_setup_url.split('https')[2]
auth = urlparse.parse_qs(urlparse.urlparse(monitor_url_with_auth).path)
# check that monitor-base-url for all partitions in the tree are accessible
# with published username and password
partition_with_monitor_base_url_count = 0
for partition_information in self.get_all_instantiated_partition_list():
connection_xml = partition_information.get('connection_xml')
if not connection_xml:
continue
connection_dict = slapos.util.xml2dict(
partition_information['connection_xml'].encode('utf-8'))
monitor_base_url = connection_dict.get('monitor-base-url')
if not monitor_base_url:
continue
result = requests.get(
monitor_base_url, verify=False, auth=(
auth['username'][0],
auth['password'][0])
)
self.assertEqual(
httplib.OK,
result.status_code
)
partition_with_monitor_base_url_count += 1
self.assertEqual(
self.expected_partition_with_monitor_base_url_count,
partition_with_monitor_base_url_count
)
class TestAccessDefault(MonitorAccessMixin, InstanceTestCase):
__partition_reference__ = 'ad'
expected_partition_with_monitor_base_url_count = 1
def test(self):
connection_parameter_dict = self.computer_partition\
.getConnectionParameterDict()
result = requests.get(connection_parameter_dict['url'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
self.assertFalse('url-additional' in connection_parameter_dict)
class TestAccessDefaultAdditional(MonitorAccessMixin, InstanceTestCase):
__partition_reference__ = 'ada'
expected_partition_with_monitor_base_url_count = 1
@classmethod
def getInstanceParameterDict(cls):
return {
'frontend-additional-instance-guid': 'SOMETHING'
}
def test(self):
connection_parameter_dict = self.computer_partition\
.getConnectionParameterDict()
result = requests.get(connection_parameter_dict['url'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
result = requests.get(
connection_parameter_dict['url-additional'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
class TestAccessKvmCluster(MonitorAccessMixin, InstanceTestCase):
__partition_reference__ = 'akc'
expected_partition_with_monitor_base_url_count = 2
@classmethod
def getInstanceSoftwareType(cls):
return 'kvm-cluster'
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({
"kvm-partition-dict": {
"KVM0": {
"disable-ansible-promise": True
}
}
})}
def test(self):
connection_parameter_dict = self.computer_partition\
.getConnectionParameterDict()
result = requests.get(connection_parameter_dict['kvm0-url'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
self.assertFalse('kvm0-url-additional' in connection_parameter_dict)
class TestAccessKvmClusterAdditional(MonitorAccessMixin, InstanceTestCase):
__partition_reference__ = 'akca'
expected_partition_with_monitor_base_url_count = 2
@classmethod
def getInstanceSoftwareType(cls):
return 'kvm-cluster'
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({
"frontend": {
'frontend-additional-instance-guid': 'SOMETHING',
},
"kvm-partition-dict": {
"KVM0": {
"disable-ansible-promise": True,
}
}
})}
def test(self):
connection_parameter_dict = self.computer_partition\
.getConnectionParameterDict()
result = requests.get(connection_parameter_dict['kvm0-url'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
result = requests.get(
connection_parameter_dict['kvm0-url-additional'], verify=False)
self.assertEqual(
httplib.OK,
result.status_code
)
self.assertTrue('<title>noVNC</title>' in result.text)
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.monitor' name = 'slapos.test.monitor'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -26,33 +26,17 @@ ...@@ -26,33 +26,17 @@
############################################################################## ##############################################################################
import os import os
import shutil
import urlparse
import tempfile
import requests
import socket
import StringIO
import subprocess
import json
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
# for development: debugging logs and install Ctrl+C handler from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class InstanceTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class ServicesTestCase(SlapOSInstanceTestCase):
class ServicesTestCase(InstanceTestCase):
def test_hashes(self): def test_hashes(self):
hash_files = [ hash_files = [
...@@ -63,9 +47,9 @@ class ServicesTestCase(InstanceTestCase): ...@@ -63,9 +47,9 @@ class ServicesTestCase(InstanceTestCase):
'crond-{hash}-on-watch', 'crond-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [os.path.join(self.computer_partition_root_path, path)
for path in hash_files] for path in hash_files]
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -31,37 +31,45 @@ import json ...@@ -31,37 +31,45 @@ import json
import glob import glob
import re import re
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
def subprocess_status_output(*args, **kwargs): setUpModule, InstanceTestCase = makeModuleSetUpAndTestCaseClass(
prc = subprocess.Popen( os.path.abspath(
stdout=subprocess.PIPE, os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
stderr=subprocess.STDOUT,
*args,
**kwargs)
out, err = prc.communicate()
return prc.returncode, out
class InstanceTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
def getNextcloudConfig(self, config_dict={}): class NextCloudTestCase(InstanceTestCase):
# calculated in setUp
partition_dir = None
nextcloud_path = None
def setUp(self):
# we want full diff when assertions fail
self.maxDiff = None self.maxDiff = None
# lookup the partition in which nextcloud was installed
partition_path_list = glob.glob(os.path.join(
self.slap.instance_directory, '*'))
for partition_path in partition_path_list:
path = os.path.join(partition_path, 'srv/www')
if os.path.exists(path):
self.nextcloud_path = path
self.instance_folder = partition_path
break
self.assertTrue(
self.nextcloud_path,
"Nextcloud path not found in %r" % (partition_path_list,))
def getNextcloudConfig(self, config_dict={}):
data_dict = dict( data_dict = dict(
datadirectory=self.partition_dir + "/srv/data", datadirectory=self.partition_dir + "/srv/data",
dbhost="%s:2099" % self.config['ipv4_address'], dbhost="%s:2099" % self._ipv4_address,
dbname="nextcloud", dbname="nextcloud",
dbpassword="insecure", dbpassword="insecure",
dbport="", dbport="",
dbuser="nextcloud", dbuser="nextcloud",
mail_domain="nextcloud@example.com", mail_domain="nextcloud@example.com",
...@@ -72,36 +80,36 @@ class InstanceTestCase(utils.SlapOSInstanceTestCase): ...@@ -72,36 +80,36 @@ class InstanceTestCase(utils.SlapOSInstanceTestCase):
mail_smtpport="587", mail_smtpport="587",
mail_smtppassword="", mail_smtppassword="",
mail_smtpname="", mail_smtpname="",
cli_url="https://[%s]:9988/" % self.config['ipv6_address'], cli_url="https://[%s]:9988/" % self._ipv6_address,
partition_dir=self.partition_dir, partition_dir=self.partition_dir,
trusted_domain_list=json.dumps(["[%s]:9988" % self.config['ipv6_address']]), trusted_domain_list=json.dumps(["[%s]:9988" % self._ipv6_address]),
trusted_proxy_list=[], trusted_proxy_list=[],
) )
data_dict.update(config_dict) data_dict.update(config_dict)
template = """{ template = """{
"activity_expire_days": 14, "activity_expire_days": 14,
"auth.bruteforce.protection.enabled": true, "auth.bruteforce.protection.enabled": true,
"blacklisted_files": [ "blacklisted_files": [
".htaccess", ".htaccess",
"Thumbs.db", "Thumbs.db",
"thumbs.db" "thumbs.db"
], ],
"cron_log": true, "cron_log": true,
"csrf.optout": [ "csrf.optout": [
"/^WebDAVFS/", "/^WebDAVFS/",
"/^Microsoft-WebDAV-MiniRedir/", "/^Microsoft-WebDAV-MiniRedir/",
"/^\\\\.jio_documents/" "/^\\\\.jio_documents/"
], ],
"datadirectory": "%(datadirectory)s", "datadirectory": "%(datadirectory)s",
"dbhost": "%(dbhost)s", "dbhost": "%(dbhost)s",
"dbname": "%(dbname)s", "dbname": "%(dbname)s",
"dbpassword": "%(dbpassword)s", "dbpassword": "%(dbpassword)s",
"dbport": "", "dbport": "",
"dbtableprefix": "oc_", "dbtableprefix": "oc_",
"dbtype": "mysql", "dbtype": "mysql",
"dbuser": "%(dbuser)s", "dbuser": "%(dbuser)s",
"enable_previews": true, "enable_previews": true,
"enabledPreviewProviders": [ "enabledPreviewProviders": [
"OC\\\\Preview\\\\PNG", "OC\\\\Preview\\\\PNG",
"OC\\\\Preview\\\\JPEG", "OC\\\\Preview\\\\JPEG",
...@@ -114,58 +122,58 @@ class InstanceTestCase(utils.SlapOSInstanceTestCase): ...@@ -114,58 +122,58 @@ class InstanceTestCase(utils.SlapOSInstanceTestCase):
"OC\\\\Preview\\\\TXT", "OC\\\\Preview\\\\TXT",
"OC\\\\Preview\\\\MarkDown" "OC\\\\Preview\\\\MarkDown"
], ],
"filelocking.enabled": "true", "filelocking.enabled": "true",
"filesystem_check_changes": 0, "filesystem_check_changes": 0,
"forwarded_for_headers": [ "forwarded_for_headers": [
"HTTP_X_FORWARDED" "HTTP_X_FORWARDED"
], ],
"htaccess.RewriteBase": "/", "htaccess.RewriteBase": "/",
"installed": true, "installed": true,
"integrity.check.disabled": false, "integrity.check.disabled": false,
"knowledgebaseenabled": false, "knowledgebaseenabled": false,
"log_rotate_size": 104857600, "log_rotate_size": 104857600,
"logfile": "%(datadirectory)s/nextcloud.log", "logfile": "%(datadirectory)s/nextcloud.log",
"loglevel": 2, "loglevel": 2,
"mail_domain": "%(mail_domain)s", "mail_domain": "%(mail_domain)s",
"mail_from_address": "%(mail_from_address)s", "mail_from_address": "%(mail_from_address)s",
"mail_sendmailmode": "smtp", "mail_sendmailmode": "smtp",
"mail_smtpauth": %(mail_smtpauth)s, "mail_smtpauth": %(mail_smtpauth)s,
"mail_smtpauthtype": "%(mail_smtpauthtype)s", "mail_smtpauthtype": "%(mail_smtpauthtype)s",
"mail_smtphost": "%(mail_smtphost)s", "mail_smtphost": "%(mail_smtphost)s",
"mail_smtpmode": "smtp", "mail_smtpmode": "smtp",
"mail_smtpname": "%(mail_smtpname)s", "mail_smtpname": "%(mail_smtpname)s",
"mail_smtppassword": "%(mail_smtppassword)s", "mail_smtppassword": "%(mail_smtppassword)s",
"mail_smtpport": "%(mail_smtpport)s", "mail_smtpport": "%(mail_smtpport)s",
"mail_smtpsecure": "tls", "mail_smtpsecure": "tls",
"maintenance": false, "maintenance": false,
"memcache.locking": "\\\\OC\\\\Memcache\\\\Redis", "memcache.locking": "\\\\OC\\\\Memcache\\\\Redis",
"memcache.local": "\\\\OC\\\\Memcache\\\\APCu", "memcache.local": "\\\\OC\\\\Memcache\\\\APCu",
"memcache.distributed": "\\\\OC\\\\Memcache\\\\Redis", "memcache.distributed": "\\\\OC\\\\Memcache\\\\Redis",
"mysql.utf8mb4": true, "mysql.utf8mb4": true,
"overwrite.cli.url": "%(cli_url)s", "overwrite.cli.url": "%(cli_url)s",
"overwriteprotocol": "https", "overwriteprotocol": "https",
"preview_max_scale_factor": 1, "preview_max_scale_factor": 1,
"preview_max_x": 1024, "preview_max_x": 1024,
"preview_max_y": 768, "preview_max_y": 768,
"quota_include_external_storage": false, "quota_include_external_storage": false,
"redis": { "redis": {
"host": "%(partition_dir)s/srv/redis/redis.socket", "host": "%(partition_dir)s/srv/redis/redis.socket",
"port": 0, "port": 0,
"timeout": 0 "timeout": 0
}, },
"share_folder": "/Shares", "share_folder": "/Shares",
"skeletondirectory": "", "skeletondirectory": "",
"theme": "", "theme": "",
"trashbin_retention_obligation": "auto, 7", "trashbin_retention_obligation": "auto, 7",
"trusted_domains": %(trusted_domain_list)s, "trusted_domains": %(trusted_domain_list)s,
"trusted_proxies": %(trusted_proxy_list)s, "trusted_proxies": %(trusted_proxy_list)s,
"updater.release.channel": "stable" "updater.release.channel": "stable"
}""" }"""
return json.loads(template % data_dict) return json.loads(template % data_dict)
class ServicesTestCase(InstanceTestCase): class TestServices(NextCloudTestCase):
def test_process_list(self): def test_process_list(self):
hash_list = [ hash_list = [
...@@ -185,8 +193,8 @@ class ServicesTestCase(InstanceTestCase): ...@@ -185,8 +193,8 @@ class ServicesTestCase(InstanceTestCase):
'redis-on-watch', 'redis-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_name_list = [process['name'] process_name_list = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_file_list = [os.path.join(self.computer_partition_root_path, path) hash_file_list = [os.path.join(self.computer_partition_root_path, path)
...@@ -199,24 +207,16 @@ class ServicesTestCase(InstanceTestCase): ...@@ -199,24 +207,16 @@ class ServicesTestCase(InstanceTestCase):
self.assertIn(expected_process_name, process_name_list) self.assertIn(expected_process_name, process_name_list)
def test_nextcloud_installation(self): def test_nextcloud_installation(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*')) can_install_path = os.path.join(self.nextcloud_path, 'config/CAN_INSTALL')
nextcloud_path = None
for partition_path in partition_path_list:
path = os.path.join(partition_path, 'srv/www')
if os.path.exists(path):
nextcloud_path = path
instance_folder = partition_path
break
can_install_path = os.path.join(nextcloud_path, 'config/CAN_INSTALL')
self.assertTrue(os.path.exists(nextcloud_path)) self.assertTrue(os.path.exists(self.nextcloud_path))
self.assertFalse(os.path.exists(can_install_path)) self.assertFalse(os.path.exists(can_install_path))
self.assertTrue(os.path.exists(os.path.join(nextcloud_path, 'config/config.php'))) self.assertTrue(os.path.exists(os.path.join(self.nextcloud_path, 'config/config.php')))
php_bin = os.path.join(instance_folder, 'bin/php') php_bin = os.path.join(self.partition_dir, 'bin/php')
nextcloud_status = subprocess.check_output([ nextcloud_status = subprocess.check_output([
php_bin, php_bin,
os.path.join(nextcloud_path, 'occ'), os.path.join(self.nextcloud_path, 'occ'),
'status', 'status',
'--output', '--output',
'json']) 'json'])
...@@ -224,22 +224,13 @@ class ServicesTestCase(InstanceTestCase): ...@@ -224,22 +224,13 @@ class ServicesTestCase(InstanceTestCase):
self.assertTrue(json_status['installed'], True) self.assertTrue(json_status['installed'], True)
def test_nextcloud_config(self): def test_nextcloud_config(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*')) config_file = os.path.join(self.nextcloud_path, 'config/config.php')
nextcloud_path = None php_script = os.path.join(self.partition_dir, 'test.php')
for partition_path in partition_path_list:
path = os.path.join(partition_path, 'srv/www')
if os.path.exists(path):
nextcloud_path = path
instance_folder = partition_path
break
config_file = os.path.join(nextcloud_path, 'config/config.php')
php_script = os.path.join(instance_folder, 'test.php')
with open(php_script, 'w') as f: with open(php_script, 'w') as f:
f.write("<?php include('%s'); echo json_encode($CONFIG); ?>" % config_file) f.write("<?php include('%s'); echo json_encode($CONFIG); ?>" % config_file)
self.partition_dir = instance_folder php_bin = os.path.join(self.partition_dir, 'bin/php')
php_bin = os.path.join(instance_folder, 'bin/php') occ = os.path.join(self.nextcloud_path, 'occ')
occ = os.path.join(nextcloud_path, 'occ')
config_result = subprocess.check_output([ config_result = subprocess.check_output([
php_bin, php_bin,
'-f', '-f',
...@@ -277,7 +268,7 @@ class ServicesTestCase(InstanceTestCase): ...@@ -277,7 +268,7 @@ class ServicesTestCase(InstanceTestCase):
"turn_servers" "turn_servers"
]) ])
self.assertEqual(turn_config.strip(), '[{"server":"","secret":"","protocols":"udp,tcp"}]') self.assertEqual(turn_config.strip(), '[{"server":"","secret":"","protocols":"udp,tcp"}]')
news_config_file = os.path.join(instance_folder, 'srv/data/news/config/config.ini') news_config_file = os.path.join(self.instance_folder, 'srv/data/news/config/config.ini')
with open(news_config_file) as f: with open(news_config_file) as f:
config = f.read() config = f.read()
regex = r"(useCronUpdates\s+=\s+false)" regex = r"(useCronUpdates\s+=\s+false)"
...@@ -285,64 +276,8 @@ class ServicesTestCase(InstanceTestCase): ...@@ -285,64 +276,8 @@ class ServicesTestCase(InstanceTestCase):
self.assertNotEqual(result, None) self.assertNotEqual(result, None)
def test_nextcloud_promises(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*'))
nextcloud_path = None
for partition_path in partition_path_list:
path = os.path.join(partition_path, 'srv/www')
if os.path.exists(path):
nextcloud_path = path
instance_folder = partition_path
break
promise_path_list = glob.glob(os.path.join(instance_folder, 'etc/plugin/*.py'))
promise_name_list = [x for x in
os.listdir(os.path.join(instance_folder, 'etc/plugin'))
if not x.endswith('.pyc')]
partition_name = os.path.basename(instance_folder.rstrip('/'))
self.assertEqual(sorted(promise_name_list),
sorted([
"__init__.py",
"check-free-disk-space.py",
"monitor-http-frontend.py",
"apache-httpd-port-listening.py",
"buildout-%s-status.py" % partition_name,
"monitor-bootstrap-status.py",
"monitor-httpd-listening-on-tcp.py"
]))
ignored_plugin_list = [
'__init__.py',
'monitor-http-frontend.py',
]
runpromise_bin = os.path.join(
self.software_path, 'bin', 'monitor.runpromise')
monitor_conf = os.path.join(instance_folder, 'etc', 'monitor.conf')
msg = []
status = 0
for plugin_path in promise_path_list:
plugin_name = os.path.basename(plugin_path)
if plugin_name in ignored_plugin_list:
continue
plugin_status, plugin_result = subprocess_status_output([
runpromise_bin,
'-c', monitor_conf,
'--run-only', plugin_name,
'--force',
'--check-anomaly'
])
status += plugin_status
if plugin_status == 1:
msg.append(plugin_result)
# sanity check
if 'Checking promise %s' % plugin_name not in plugin_result:
plugin_status = 1
msg.append(plugin_result)
msg = ''.join(msg).strip()
self.assertEqual(status, 0, msg)
class ParametersTestCase(InstanceTestCase): class TestNextCloudParameters(NextCloudTestCase):
@classmethod @classmethod
def getInstanceParameterDict(cls): def getInstanceParameterDict(cls):
return { return {
...@@ -365,22 +300,13 @@ class ParametersTestCase(InstanceTestCase): ...@@ -365,22 +300,13 @@ class ParametersTestCase(InstanceTestCase):
} }
def test_nextcloud_config_with_parameters(self): def test_nextcloud_config_with_parameters(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*')) config_file = os.path.join(self.nextcloud_path, 'config/config.php')
nextcloud_path = None php_script = os.path.join(self.partition_dir, 'test.php')
for partition_path in partition_path_list:
path = os.path.join(partition_path, 'srv/www')
if os.path.exists(path):
nextcloud_path = path
instance_folder = partition_path
break
config_file = os.path.join(nextcloud_path, 'config/config.php')
php_script = os.path.join(instance_folder, 'test.php')
with open(php_script, 'w') as f: with open(php_script, 'w') as f:
f.write("<?php include('%s'); echo json_encode($CONFIG); ?>" % config_file) f.write("<?php include('%s'); echo json_encode($CONFIG); ?>" % config_file)
self.partition_dir = instance_folder php_bin = os.path.join(self.partition_dir, 'bin/php')
php_bin = os.path.join(instance_folder, 'bin/php') occ = os.path.join(self.nextcloud_path, 'occ')
occ = os.path.join(nextcloud_path, 'occ')
config_result = subprocess.check_output([ config_result = subprocess.check_output([
php_bin, php_bin,
'-f', '-f',
...@@ -404,7 +330,7 @@ class ParametersTestCase(InstanceTestCase): ...@@ -404,7 +330,7 @@ class ParametersTestCase(InstanceTestCase):
cli_url="nextcloud.example.com", cli_url="nextcloud.example.com",
partition_dir=self.partition_dir, partition_dir=self.partition_dir,
trusted_domain_list=json.dumps([ trusted_domain_list=json.dumps([
"[%s]:9988" % self.config['ipv6_address'], "[%s]:9988" % self._ipv6_address,
"nextcloud.example.com", "nextcloud.example.com",
"nextcloud.proxy.com" "nextcloud.proxy.com"
]), ]),
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
cls.instance_path = os.path.join(
cls.config['working_directory'],
'inst')
cls.software_path = os.path.realpath(os.path.join(
cls.computer_partition_root_path, 'software_release'))
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.plantuml' name = 'slapos.test.plantuml'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -35,21 +35,13 @@ from PIL import Image ...@@ -35,21 +35,13 @@ from PIL import Image
import requests import requests
import plantuml import plantuml
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, PlantUMLTestCase = makeModuleSetUpAndTestCaseClass(
class PlantUMLTestCase(utils.SlapOSInstanceTestCase): os.path.abspath(
@classmethod os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class TestSimpleDiagram(PlantUMLTestCase): class TestSimpleDiagram(PlantUMLTestCase):
...@@ -165,8 +157,8 @@ class ServicesTestCase(PlantUMLTestCase): ...@@ -165,8 +157,8 @@ class ServicesTestCase(PlantUMLTestCase):
'tomcat-instance-{hash}-on-watch', 'tomcat-instance-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [os.path.join(self.computer_partition_root_path, path)
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the `slapos.core.XXX` computer partition instance.
* `computer_partition_root_path`: the path of the instance root directory,
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except BaseException:
cls.stopSlapOSProcesses()
cls.setUp = lambda self: self.fail('Setup Class failed.')
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -120,7 +120,7 @@ monitor-password = $${monitor-publish-parameters:monitor-password} ...@@ -120,7 +120,7 @@ monitor-password = $${monitor-publish-parameters:monitor-password}
##################### #####################
# Power DNS Slave configuration # Power DNS Slave configuration
# #
{% set slave_instance_list = json_module.loads(slapparameter_dict.get('extra_slave_instance_list', '')) %} {% set slave_instance_list = json_module.loads(slapparameter_dict.get('extra_slave_instance_list', '[]')) %}
# Iter through slave list to prepare configuration # Iter through slave list to prepare configuration
{% for slave in slave_instance_list %} {% for slave in slave_instance_list %}
......
...@@ -25,7 +25,7 @@ mode = 0644 ...@@ -25,7 +25,7 @@ mode = 0644
[template-powerdns] [template-powerdns]
recipe = slapos.recipe.template recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance-powerdns.cfg url = ${:_profile_base_location_}/instance-powerdns.cfg
md5sum = 5077ba344b641fa9703f9872a974d3d7 md5sum = f0d87be6df84f23c811638ce9d5f60ea
output = ${buildout:directory}/template-powerdns.cfg output = ${buildout:directory}/template-powerdns.cfg
mode = 0644 mode = 0644
......
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.powerdns' name = 'slapos.test.powerdns'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -26,33 +26,22 @@ ...@@ -26,33 +26,22 @@
############################################################################## ##############################################################################
import os import os
import shutil
import urlparse
import tempfile
import requests
import socket
import StringIO
import subprocess
import json
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class InstanceTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class PowerDNSTestCase(SlapOSInstanceTestCase):
# power dns uses sockets and need shorter paths on test nodes.
__partition_reference__ = 'pdns'
class ServicesTestCase(InstanceTestCase):
class ServicesTestCase(PowerDNSTestCase):
def test_hashes(self): def test_hashes(self):
hash_files = [ hash_files = [
...@@ -62,9 +51,9 @@ class ServicesTestCase(InstanceTestCase): ...@@ -62,9 +51,9 @@ class ServicesTestCase(InstanceTestCase):
'pdns-{hash}-on-watch', 'pdns-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [os.path.join(self.computer_partition_root_path, path)
for path in hash_files] for path in hash_files]
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -25,30 +25,30 @@ ...@@ -25,30 +25,30 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.proftpd' name = 'slapos.test.proftpd'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(
version=version, name=name,
description="Test for SlapOS' ProFTPd", version=version,
long_description=long_description, description="Test for SlapOS' ProFTPd",
long_description_content_type='text/markdown', long_description=long_description,
maintainer="Nexedi", long_description_content_type='text/markdown',
maintainer_email="info@nexedi.com", maintainer="Nexedi",
url="https://lab.nexedi.com/nexedi/slapos", maintainer_email="info@nexedi.com",
packages=find_packages(), url="https://lab.nexedi.com/nexedi/slapos",
install_requires=[ packages=find_packages(),
install_requires=[
'slapos.core', 'slapos.core',
'slapos.libnetworkcache', 'slapos.libnetworkcache',
'erp5.util', 'erp5.util',
'pysftp', 'pysftp',
'supervisor', 'supervisor',
'psutil', 'psutil',
], ],
zip_safe=True, zip_safe=True,
test_suite='test', test_suite='test',
) )
...@@ -37,22 +37,15 @@ import psutil ...@@ -37,22 +37,15 @@ import psutil
from paramiko.ssh_exception import SSHException from paramiko.ssh_exception import SSHException
from paramiko.ssh_exception import AuthenticationException from paramiko.ssh_exception import AuthenticationException
import utils from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from slapos.testing.utils import findFreeTCPPort
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
class ProFTPdTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class ProFTPdTestCase(SlapOSInstanceTestCase):
def _getConnection(self, username=None, password=None, hostname=None): def _getConnection(self, username=None, password=None, hostname=None):
"""Returns a pysftp connection connected to the SFTP """Returns a pysftp connection connected to the SFTP
...@@ -77,7 +70,7 @@ class ProFTPdTestCase(utils.SlapOSInstanceTestCase): ...@@ -77,7 +70,7 @@ class ProFTPdTestCase(utils.SlapOSInstanceTestCase):
class TestSFTPListen(ProFTPdTestCase): class TestSFTPListen(ProFTPdTestCase):
def test_listen_on_ipv4(self): def test_listen_on_ipv4(self):
self.assertTrue(self._getConnection(hostname=self.config['ipv4_address'])) self.assertTrue(self._getConnection(hostname=self._ipv4_address))
def test_does_not_listen_on_all_ip(self): def test_does_not_listen_on_all_ip(self):
with self.assertRaises(SSHException): with self.assertRaises(SSHException):
...@@ -115,20 +108,22 @@ class TestSFTPOperations(ProFTPdTestCase): ...@@ -115,20 +108,22 @@ class TestSFTPOperations(ProFTPdTestCase):
# download the file again, it should have same content # download the file again, it should have same content
tempdir = tempfile.mkdtemp() tempdir = tempfile.mkdtemp()
self.addCleanup(lambda : shutil.rmtree(tempdir)) self.addCleanup(lambda: shutil.rmtree(tempdir))
local_file = os.path.join(tempdir, 'testfile') local_file = os.path.join(tempdir, 'testfile')
retrieve_same_file = sftp.get('testfile', local_file) sftp.get('testfile', local_file)
with open(local_file) as f: with open(local_file) as f:
self.assertEqual(f.read(), "Hello FTP !") self.assertEqual(f.read(), "Hello FTP !")
def test_uploaded_file_not_visible_until_fully_uploaded(self): def test_uploaded_file_not_visible_until_fully_uploaded(self):
test_self = self test_self = self
class PartialFile(StringIO.StringIO): class PartialFile(StringIO.StringIO):
def read(self, *args): def read(self, *args):
# file is not visible yet # file is not visible yet
test_self.assertNotIn('destination', os.listdir(test_self.upload_dir)) test_self.assertNotIn('destination', os.listdir(test_self.upload_dir))
# it's just a hidden file # it's just a hidden file
test_self.assertEqual(['.in.destination.'], os.listdir(test_self.upload_dir)) test_self.assertEqual(
['.in.destination.'], os.listdir(test_self.upload_dir))
return StringIO.StringIO.read(self, *args) return StringIO.StringIO.read(self, *args)
with self._getConnection() as sftp: with self._getConnection() as sftp:
...@@ -140,13 +135,16 @@ class TestSFTPOperations(ProFTPdTestCase): ...@@ -140,13 +135,16 @@ class TestSFTPOperations(ProFTPdTestCase):
def test_partial_upload_are_deleted(self): def test_partial_upload_are_deleted(self):
test_self = self test_self = self
with self._getConnection() as sftp: with self._getConnection() as sftp:
class ErrorFile(StringIO.StringIO): class ErrorFile(StringIO.StringIO):
def read(self, *args): def read(self, *args):
# at this point, file is already created on server # at this point, file is already created on server
test_self.assertEqual(['.in.destination.'], os.listdir(test_self.upload_dir)) test_self.assertEqual(
['.in.destination.'], os.listdir(test_self.upload_dir))
# simulate a connection closed # simulate a connection closed
sftp.sftp_client.close() sftp.sftp_client.close()
return "something that will not be sent to server" return "something that will not be sent to server"
with self.assertRaises(IOError): with self.assertRaises(IOError):
sftp.sftp_client.putfo(ErrorFile(), "destination") sftp.sftp_client.putfo(ErrorFile(), "destination")
# no half uploaded file is kept # no half uploaded file is kept
...@@ -164,12 +162,14 @@ class TestSFTPOperations(ProFTPdTestCase): ...@@ -164,12 +162,14 @@ class TestSFTPOperations(ProFTPdTestCase):
class TestUserManagement(ProFTPdTestCase): class TestUserManagement(ProFTPdTestCase):
def test_user_can_be_added_from_script(self): def test_user_can_be_added_from_script(self):
with self.assertRaisesRegexp(AuthenticationException, 'Authentication failed'): with self.assertRaisesRegexp(AuthenticationException,
'Authentication failed'):
self._getConnection(username='bob', password='secret') self._getConnection(username='bob', password='secret')
subprocess.check_call( subprocess.check_call(
'echo secret | %s/bin/ftpasswd --name=bob --stdin' % self.computer_partition_root_path, 'echo secret | %s/bin/ftpasswd --name=bob --stdin' %
shell=True) self.computer_partition_root_path,
shell=True)
self.assertTrue(self._getConnection(username='bob', password='secret')) self.assertTrue(self._getConnection(username='bob', password='secret'))
...@@ -177,7 +177,8 @@ class TestBan(ProFTPdTestCase): ...@@ -177,7 +177,8 @@ class TestBan(ProFTPdTestCase):
def test_client_are_banned_after_5_wrong_passwords(self): def test_client_are_banned_after_5_wrong_passwords(self):
# Simulate failed 5 login attempts # Simulate failed 5 login attempts
for i in range(5): for i in range(5):
with self.assertRaisesRegexp(AuthenticationException, 'Authentication failed'): with self.assertRaisesRegexp(AuthenticationException,
'Authentication failed'):
self._getConnection(password='wrong') self._getConnection(password='wrong')
# after that, even with a valid password we cannot connect # after that, even with a valid password we cannot connect
...@@ -185,17 +186,19 @@ class TestBan(ProFTPdTestCase): ...@@ -185,17 +186,19 @@ class TestBan(ProFTPdTestCase):
self._getConnection() self._getConnection()
# ban event is logged # ban event is logged
with open(os.path.join( with open(os.path.join(self.computer_partition_root_path,
self.computer_partition_root_path, 'var', 'log', 'proftpd-ban.log')) as ban_log_file: 'var',
'log',
'proftpd-ban.log')) as ban_log_file:
self.assertRegexpMatches( self.assertRegexpMatches(
ban_log_file.readlines()[-1], ban_log_file.readlines()[-1],
'login from host .* denied due to host ban') 'login from host .* denied due to host ban')
class TestInstanceParameterPort(ProFTPdTestCase): class TestInstanceParameterPort(ProFTPdTestCase):
@classmethod @classmethod
def getInstanceParameterDict(cls): def getInstanceParameterDict(cls):
cls.free_port = utils.findFreeTCPPort(cls.config['ipv4_address']) cls.free_port = findFreeTCPPort(cls._ipv4_address)
return {'port': cls.free_port} return {'port': cls.free_port}
def test_instance_parameter_port(self): def test_instance_parameter_port(self):
...@@ -211,23 +214,26 @@ class TestFilesAndSocketsInInstanceDir(ProFTPdTestCase): ...@@ -211,23 +214,26 @@ class TestFilesAndSocketsInInstanceDir(ProFTPdTestCase):
def setUp(self): def setUp(self):
"""sets `self.proftpdProcess` to `psutil.Process` for the running proftpd in the instance. """sets `self.proftpdProcess` to `psutil.Process` for the running proftpd in the instance.
""" """
all_process_info = self.getSupervisorRPCServer().supervisor.getAllProcessInfo() with self.slap.instance_supervisor_rpc as supervisor:
all_process_info = supervisor.getAllProcessInfo()
# there is only one process in this instance # there is only one process in this instance
process_info, = [p for p in all_process_info if p['name'] != 'watchdog'] process_info, = [p for p in all_process_info if p['name'] != 'watchdog']
process = psutil.Process(process_info['pid']) process = psutil.Process(process_info['pid'])
self.assertEqual('proftpd', process.name()) # sanity check self.assertEqual('proftpd', process.name()) # sanity check
self.proftpdProcess = process self.proftpdProcess = process
def test_only_write_file_in_instance_dir(self): def test_only_write_file_in_instance_dir(self):
self.assertEqual( self.assertEqual(
[], [],
[f for f in self.proftpdProcess.open_files() [
if f.mode != 'r' f for f in self.proftpdProcess.open_files() if f.mode != 'r'
if not f.path.startswith(self.computer_partition_root_path)]) if not f.path.startswith(self.computer_partition_root_path)
])
def test_only_unix_socket_in_instance_dir(self): def test_only_unix_socket_in_instance_dir(self):
self.assertEqual( self.assertEqual(
[], [],
[s for s in self.proftpdProcess.connections('unix') [
if not s.laddr.startswith(self.computer_partition_root_path)]) s for s in self.proftpdProcess.connections('unix')
if not s.laddr.startswith(self.computer_partition_root_path)
])
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -25,30 +25,30 @@ ...@@ -25,30 +25,30 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.re6stnet' name = 'slapos.test.re6stnet'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(
version=version, name=name,
description="Test for SlapOS' Re6stnet", version=version,
long_description=long_description, description="Test for SlapOS' Re6stnet",
long_description_content_type='text/markdown', long_description=long_description,
maintainer="Nexedi", long_description_content_type='text/markdown',
maintainer_email="info@nexedi.com", maintainer="Nexedi",
url="https://lab.nexedi.com/nexedi/slapos", maintainer_email="info@nexedi.com",
packages=find_packages(), url="https://lab.nexedi.com/nexedi/slapos",
install_requires=[ packages=find_packages(),
install_requires=[
'slapos.core', 'slapos.core',
'slapos.cookbook', 'slapos.cookbook',
'slapos.libnetworkcache', 'slapos.libnetworkcache',
'erp5.util', 'erp5.util',
'supervisor', 'supervisor',
'psutil', 'psutil',
], ],
zip_safe=True, zip_safe=True,
test_suite='test', test_suite='test',
) )
...@@ -26,60 +26,39 @@ ...@@ -26,60 +26,39 @@
############################################################################## ##############################################################################
import os import os
import shutil
import urlparse
import tempfile
import requests import requests
import socket
import StringIO
import subprocess
import json import json
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
SLAPOS_TEST_IPV4 = os.environ['SLAPOS_TEST_IPV4'] setUpModule, Re6stnetTestCase = makeModuleSetUpAndTestCaseClass(
SLAPOS_TEST_IPV6 = os.environ['SLAPOS_TEST_IPV6'] os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
class Re6stnetTestCase(utils.SlapOSInstanceTestCase):
def setUp(self):
import logging
utils.SlapOSInstanceTestCase.setUp(self)
self.logger = logging.getLogger(__name__)
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class TestRe6stnetRegistry(Re6stnetTestCase): class TestRe6stnetRegistry(Re6stnetTestCase):
def test_listen(self): def test_listen(self):
connection_parameters = self.computer_partition.getConnectionParameterDict() connection_parameters = self.computer_partition.getConnectionParameterDict()
registry_url = connection_parameters['re6stry-local-url'] registry_url = connection_parameters['re6stry-local-url']
_ = requests.get(registry_url) _ = requests.get(registry_url)
class TestPortRedirection(Re6stnetTestCase):
class TestPortRedirection(Re6stnetTestCase):
def test_portredir_config(self): def test_portredir_config(self):
portredir_config_path = os.path.join(self.computer_partition_root_path, '.slapos-port-redirect') portredir_config_path = os.path.join(
self.computer_partition_root_path, '.slapos-port-redirect')
with open(portredir_config_path) as f: with open(portredir_config_path) as f:
portredir_config = json.load(f) portredir_config = json.load(f)
self.assertDictContainsSubset({ self.assertDictContainsSubset(
'srcPort': 9201, {
'destPort': 9201, 'srcPort': 9201,
}, portredir_config[0]) 'destPort': 9201,
}, portredir_config[0])
class ServicesTestCase(Re6stnetTestCase): class ServicesTestCase(Re6stnetTestCase):
...@@ -89,18 +68,21 @@ class ServicesTestCase(Re6stnetTestCase): ...@@ -89,18 +68,21 @@ class ServicesTestCase(Re6stnetTestCase):
def test_hashes(self): def test_hashes(self):
hash_files = [ hash_files = [
'software_release/buildout.cfg', 'software_release/buildout.cfg',
] ]
expected_process_names = [ expected_process_names = [
'httpd-{hash}-on-watch', 'httpd-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [
for process in supervisor.getAllProcessInfo()] process['name'] for process in supervisor.getAllProcessInfo()
]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [
for path in hash_files] os.path.join(self.computer_partition_root_path, path)
for path in hash_files
]
for name in expected_process_names: for name in expected_process_names:
h = generateHashFromFiles(hash_files) h = generateHashFromFiles(hash_files)
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -28,18 +28,20 @@ from setuptools import setup, find_packages ...@@ -28,18 +28,20 @@ from setuptools import setup, find_packages
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.seleniumserver' name = 'slapos.test.seleniumserver'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(
version=version, name=name,
description="Test for SlapOS' Selenium Server", version=version,
long_description=long_description, description="Test for SlapOS' Selenium Server",
long_description_content_type='text/markdown', long_description=long_description,
maintainer="Nexedi", long_description_content_type='text/markdown',
maintainer_email="info@nexedi.com", maintainer="Nexedi",
url="https://lab.nexedi.com/nexedi/slapos", maintainer_email="info@nexedi.com",
packages=find_packages(), url="https://lab.nexedi.com/nexedi/slapos",
install_requires=[ packages=find_packages(),
install_requires=[
'slapos.core', 'slapos.core',
'supervisor', 'supervisor',
'slapos.libnetworkcache', 'slapos.libnetworkcache',
...@@ -49,7 +51,7 @@ setup(name=name, ...@@ -49,7 +51,7 @@ setup(name=name,
'image', 'image',
'requests', 'requests',
'paramiko', 'paramiko',
], ],
zip_safe=True, zip_safe=True,
test_suite='test', test_suite='test',
) )
...@@ -47,14 +47,12 @@ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities ...@@ -47,14 +47,12 @@ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from utils import SlapOSInstanceTestCase, findFreeTCPPort from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from slapos.testing.utils import findFreeTCPPort
debug_mode = os.environ.get('SLAPOS_TEST_DEBUG') setUpModule, SeleniumServerTestCase = makeModuleSetUpAndTestCaseClass(
# for development: debugging logs and install Ctrl+C handler os.path.abspath(
if debug_mode: os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
import logging
logging.basicConfig(level=logging.DEBUG)
unittest.installHandler()
class WebServerMixin(object): class WebServerMixin(object):
...@@ -71,12 +69,14 @@ class WebServerMixin(object): ...@@ -71,12 +69,14 @@ class WebServerMixin(object):
- upload a file and the file content will be displayed in div.uploadedfile - upload a file and the file content will be displayed in div.uploadedfile
""" """
def log_message(self, *args, **kw): def log_message(self, *args, **kw):
if debug_mode: if SeleniumServerTestCase._debug:
BaseHTTPRequestHandler.log_message(self, *args, **kw) BaseHTTPRequestHandler.log_message(self, *args, **kw)
def do_GET(self): def do_GET(self):
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
self.wfile.write(''' self.wfile.write(
'''
<html> <html>
<title>Test page</title> <title>Test page</title>
<body> <body>
...@@ -87,18 +87,22 @@ class WebServerMixin(object): ...@@ -87,18 +87,22 @@ class WebServerMixin(object):
</form> </form>
</body> </body>
</html>''') </html>''')
def do_POST(self): def do_POST(self):
form = cgi.FieldStorage( form = cgi.FieldStorage(
fp=self.rfile, fp=self.rfile,
headers=self.headers, headers=self.headers,
environ={'REQUEST_METHOD':'POST', environ={
'CONTENT_TYPE':self.headers['Content-Type'],}) 'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type'],
})
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
file_data = 'no file' file_data = 'no file'
if form.has_key('f'): if form.has_key('f'):
file_data = form['f'].file.read() file_data = form['f'].file.read()
self.wfile.write(''' self.wfile.write(
'''
<html> <html>
<title>%s</title> <title>%s</title>
<div>%s</div> <div>%s</div>
...@@ -128,8 +132,9 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -128,8 +132,9 @@ class BrowserCompatibilityMixin(WebServerMixin):
def setUp(self): def setUp(self):
super(BrowserCompatibilityMixin, self).setUp() super(BrowserCompatibilityMixin, self).setUp()
self.driver = webdriver.Remote( self.driver = webdriver.Remote(
command_executor=self.computer_partition.getConnectionParameterDict()['backend-url'], command_executor=self.computer_partition.getConnectionParameterDict()
desired_capabilities=self.desired_capabilities) ['backend-url'],
desired_capabilities=self.desired_capabilities)
def tearDown(self): def tearDown(self):
self.driver.quit() self.driver.quit()
...@@ -142,7 +147,7 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -142,7 +147,7 @@ class BrowserCompatibilityMixin(WebServerMixin):
def test_simple_submit_scenario(self): def test_simple_submit_scenario(self):
self.driver.get(self.server_url) self.driver.get(self.server_url)
input_element = WebDriverWait(self.driver, 3).until( input_element = WebDriverWait(self.driver, 3).until(
EC.visibility_of_element_located((By.NAME, 'q'))) EC.visibility_of_element_located((By.NAME, 'q')))
input_element.send_keys(self.id()) input_element.send_keys(self.id())
input_element.submit() input_element.submit()
...@@ -158,9 +163,7 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -158,9 +163,7 @@ class BrowserCompatibilityMixin(WebServerMixin):
self.driver.find_element_by_xpath('//input[@name="f"]').send_keys(f.name) self.driver.find_element_by_xpath('//input[@name="f"]').send_keys(f.name)
self.driver.find_element_by_xpath('//input[@type="submit"]').click() self.driver.find_element_by_xpath('//input[@type="submit"]').click()
self.assertEqual( self.assertEqual(self.id(), self.driver.find_element_by_xpath('//div').text)
self.id(),
self.driver.find_element_by_xpath('//div').text)
def test_screenshot(self): def test_screenshot(self):
self.driver.get(self.server_url) self.driver.get(self.server_url)
...@@ -169,7 +172,9 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -169,7 +172,9 @@ class BrowserCompatibilityMixin(WebServerMixin):
self.assertGreater(len(screenshot.getcolors(maxcolors=512)), 2) self.assertGreater(len(screenshot.getcolors(maxcolors=512)), 2)
def test_window_and_screen_size(self): def test_window_and_screen_size(self):
size = json.loads(self.driver.execute_script(''' size = json.loads(
self.driver.execute_script(
'''
return JSON.stringify({ return JSON.stringify({
'screen.width': window.screen.width, 'screen.width': window.screen.width,
'screen.height': window.screen.height, 'screen.height': window.screen.height,
...@@ -188,7 +193,9 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -188,7 +193,9 @@ class BrowserCompatibilityMixin(WebServerMixin):
def test_resize_window(self): def test_resize_window(self):
self.driver.set_window_size(800, 900) self.driver.set_window_size(800, 900)
size = json.loads(self.driver.execute_script(''' size = json.loads(
self.driver.execute_script(
'''
return JSON.stringify({ return JSON.stringify({
'outerWidth': window.outerWidth, 'outerWidth': window.outerWidth,
'outerHeight': window.outerHeight 'outerHeight': window.outerHeight
...@@ -201,10 +208,11 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -201,10 +208,11 @@ class BrowserCompatibilityMixin(WebServerMixin):
webdriver_url = parameter_dict['backend-url'] webdriver_url = parameter_dict['backend-url']
queue = multiprocessing.Queue() queue = multiprocessing.Queue()
def _test(q, server_url): def _test(q, server_url):
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=self.desired_capabilities) desired_capabilities=self.desired_capabilities)
try: try:
driver.get(server_url) driver.get(server_url)
q.put(driver.title == 'Test page') q.put(driver.title == 'Test page')
...@@ -213,32 +221,22 @@ class BrowserCompatibilityMixin(WebServerMixin): ...@@ -213,32 +221,22 @@ class BrowserCompatibilityMixin(WebServerMixin):
nb_workers = 10 nb_workers = 10
workers = [] workers = []
for i in range(nb_workers): for _ in range(nb_workers):
worker = multiprocessing.Process( worker = multiprocessing.Process(
target=_test, target=_test, args=(queue, self.server_url))
args=(queue, self.server_url))
worker.start() worker.start()
workers.append(worker) workers.append(worker)
del worker # pylint del worker # pylint
_ = [worker.join(timeout=30) for worker in workers] _ = [worker.join(timeout=30) for worker in workers]
# terminate workers if they are still alive after 30 seconds # terminate workers if they are still alive after 30 seconds
_ = [worker.terminate() for worker in workers if worker.is_alive()] _ = [worker.terminate() for worker in workers if worker.is_alive()]
_ = [worker.join() for worker in workers] _ = [worker.join() for worker in workers]
del _ # pylint del _ # pylint
self.assertEqual( self.assertEqual(
[True] * nb_workers, [True] * nb_workers, [queue.get() for _ in range(nb_workers)])
[queue.get() for _ in range(nb_workers)])
class SeleniumServerTestCase(SlapOSInstanceTestCase):
"""Test the remote driver on a minimal web server.
"""
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase): class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase):
...@@ -249,18 +247,15 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase): ...@@ -249,18 +247,15 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase):
webdriver_url = parameter_dict['backend-url'] webdriver_url = parameter_dict['backend-url']
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=DesiredCapabilities.CHROME) desired_capabilities=DesiredCapabilities.CHROME)
driver.get(self.server_url) driver.get(self.server_url)
self.assertEqual('Test page', driver.title) self.assertEqual('Test page', driver.title)
self.assertIn( self.assertIn('Chrome', driver.execute_script('return navigator.userAgent'))
'Chrome',
driver.execute_script('return navigator.userAgent'))
self.assertNotIn( self.assertNotIn(
'Firefox', 'Firefox', driver.execute_script('return navigator.userAgent'))
driver.execute_script('return navigator.userAgent'))
driver.quit() driver.quit()
def test_firefox(self): def test_firefox(self):
...@@ -268,15 +263,14 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase): ...@@ -268,15 +263,14 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase):
webdriver_url = parameter_dict['backend-url'] webdriver_url = parameter_dict['backend-url']
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=DesiredCapabilities.FIREFOX) desired_capabilities=DesiredCapabilities.FIREFOX)
driver.get(self.server_url) driver.get(self.server_url)
self.assertEqual('Test page', driver.title) self.assertEqual('Test page', driver.title)
self.assertIn( self.assertIn(
'Firefox', 'Firefox', driver.execute_script('return navigator.userAgent'))
driver.execute_script('return navigator.userAgent'))
driver.quit() driver.quit()
def test_firefox_desired_version(self): def test_firefox_desired_version(self):
...@@ -286,16 +280,16 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase): ...@@ -286,16 +280,16 @@ class TestBrowserSelection(WebServerMixin, SeleniumServerTestCase):
desired_capabilities = DesiredCapabilities.FIREFOX.copy() desired_capabilities = DesiredCapabilities.FIREFOX.copy()
desired_capabilities['version'] = '60.0.2esr' desired_capabilities['version'] = '60.0.2esr'
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=desired_capabilities) desired_capabilities=desired_capabilities)
self.assertIn( self.assertIn(
'Gecko/20100101 Firefox/60.0', 'Gecko/20100101 Firefox/60.0',
driver.execute_script('return navigator.userAgent')) driver.execute_script('return navigator.userAgent'))
driver.quit() driver.quit()
desired_capabilities['version'] = '52.9.0esr' desired_capabilities['version'] = '52.9.0esr'
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=desired_capabilities) desired_capabilities=desired_capabilities)
self.assertIn( self.assertIn(
'Gecko/20100101 Firefox/52.0', 'Gecko/20100101 Firefox/52.0',
driver.execute_script('return navigator.userAgent')) driver.execute_script('return navigator.userAgent'))
...@@ -313,9 +307,7 @@ class TestFrontend(WebServerMixin, SeleniumServerTestCase): ...@@ -313,9 +307,7 @@ class TestFrontend(WebServerMixin, SeleniumServerTestCase):
self.assertEqual('admin', parsed.username) self.assertEqual('admin', parsed.username)
self.assertTrue(parsed.password) self.assertTrue(parsed.password)
self.assertIn( self.assertIn('Grid Console', requests.get(admin_url, verify=False).text)
'Grid Console',
requests.get(admin_url, verify=False).text)
def test_browser_use_hub(self): def test_browser_use_hub(self):
parameter_dict = self.computer_partition.getConnectionParameterDict() parameter_dict = self.computer_partition.getConnectionParameterDict()
...@@ -325,8 +317,8 @@ class TestFrontend(WebServerMixin, SeleniumServerTestCase): ...@@ -325,8 +317,8 @@ class TestFrontend(WebServerMixin, SeleniumServerTestCase):
self.assertTrue(parsed.password) self.assertTrue(parsed.password)
driver = webdriver.Remote( driver = webdriver.Remote(
command_executor=webdriver_url, command_executor=webdriver_url,
desired_capabilities=DesiredCapabilities.CHROME) desired_capabilities=DesiredCapabilities.CHROME)
driver.get(self.server_url) driver.get(self.server_url)
self.assertEqual('Test page', driver.title) self.assertEqual('Test page', driver.title)
...@@ -346,27 +338,30 @@ class TestSSHServer(SeleniumServerTestCase): ...@@ -346,27 +338,30 @@ class TestSSHServer(SeleniumServerTestCase):
self.assertEqual('ssh', parsed.scheme) self.assertEqual('ssh', parsed.scheme)
client = paramiko.SSHClient() client = paramiko.SSHClient()
class TestKeyPolicy(object): class TestKeyPolicy(object):
"""Accept server key and keep it in self.key for inspection """Accept server key and keep it in self.key for inspection
""" """
def missing_host_key(self, client, hostname, key): def missing_host_key(self, client, hostname, key):
self.key = key self.key = key
key_policy = TestKeyPolicy() key_policy = TestKeyPolicy()
client.set_missing_host_key_policy(key_policy) client.set_missing_host_key_policy(key_policy)
with contextlib.closing(client): with contextlib.closing(client):
client.connect( client.connect(
username=urlparse.urlparse(ssh_url).username, username=urlparse.urlparse(ssh_url).username,
hostname=urlparse.urlparse(ssh_url).hostname, hostname=urlparse.urlparse(ssh_url).hostname,
port=urlparse.urlparse(ssh_url).port, port=urlparse.urlparse(ssh_url).port,
pkey=self.ssh_key, pkey=self.ssh_key,
) )
# Check fingerprint from server matches the published one. # Check fingerprint from server matches the published one.
# The publish format is the raw output of ssh-keygen and is something like this: # The publish format is the raw output of ssh-keygen and is something like this:
# 521 SHA256:9aZruv3LmFizzueIFdkd78eGtzghDoPSCBXFkkrHqXE user@hostname (ECDSA) # 521 SHA256:9aZruv3LmFizzueIFdkd78eGtzghDoPSCBXFkkrHqXE user@hostname (ECDSA)
# we only want to parse SHA256:9aZruv3LmFizzueIFdkd78eGtzghDoPSCBXFkkrHqXE # we only want to parse SHA256:9aZruv3LmFizzueIFdkd78eGtzghDoPSCBXFkkrHqXE
_, fingerprint_string, _, key_type = parameter_dict['ssh-fingerprint'].split() _, fingerprint_string, _, key_type = parameter_dict[
'ssh-fingerprint'].split()
self.assertEqual(key_type, '(ECDSA)') self.assertEqual(key_type, '(ECDSA)')
fingerprint_algorithm, fingerprint = fingerprint_string.split(':', 1) fingerprint_algorithm, fingerprint = fingerprint_string.split(':', 1)
...@@ -374,18 +369,23 @@ class TestSSHServer(SeleniumServerTestCase): ...@@ -374,18 +369,23 @@ class TestSSHServer(SeleniumServerTestCase):
# Paramiko does not allow to get the fingerprint as SHA256 easily yet # Paramiko does not allow to get the fingerprint as SHA256 easily yet
# https://github.com/paramiko/paramiko/pull/1103 # https://github.com/paramiko/paramiko/pull/1103
self.assertEqual( self.assertEqual(
fingerprint, fingerprint,
# XXX with sha256, we need to remove that trailing = # XXX with sha256, we need to remove that trailing =
base64.b64encode(hashlib.new(fingerprint_algorithm, key_policy.key.asbytes()).digest())[:-1] base64.b64encode(
) hashlib.new(fingerprint_algorithm,
key_policy.key.asbytes()).digest())[:-1])
channel = client.invoke_shell() channel = client.invoke_shell()
channel.settimeout(30) channel.settimeout(30)
# apparently we sometimes need to send something on the first ssh connection received = ''
channel.send('\n') while True:
# openssh prints a warning 'Attempt to write login records by non-root user (aborting)' r = channel.recv(1024)
# so we received more than the lenght of the asserted message. if not r:
self.assertIn("Welcome to SlapOS Selenium Server.", channel.recv(100)) break
received += r
if 'Selenium Server.' in received:
break
self.assertIn("Welcome to SlapOS Selenium Server.", received)
class TestFirefox52(BrowserCompatibilityMixin, SeleniumServerTestCase): class TestFirefox52(BrowserCompatibilityMixin, SeleniumServerTestCase):
...@@ -393,7 +393,7 @@ class TestFirefox52(BrowserCompatibilityMixin, SeleniumServerTestCase): ...@@ -393,7 +393,7 @@ class TestFirefox52(BrowserCompatibilityMixin, SeleniumServerTestCase):
user_agent = 'Gecko/20100101 Firefox/52.0' user_agent = 'Gecko/20100101 Firefox/52.0'
# resizing window is not supported on firefox 52 geckodriver # resizing window is not supported on firefox 52 geckodriver
test_resize_window = unittest.expectedFailure( test_resize_window = unittest.expectedFailure(
BrowserCompatibilityMixin.test_resize_window) BrowserCompatibilityMixin.test_resize_window)
class TestFirefox60(BrowserCompatibilityMixin, SeleniumServerTestCase): class TestFirefox60(BrowserCompatibilityMixin, SeleniumServerTestCase):
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the `slapos.core.XXX` computer partition instance.
* `computer_partition_root_path`: the path of the instance root directory,
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except BaseException:
cls.stopSlapOSProcesses()
cls.setUp = lambda self: self.fail('Setup Class failed.')
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
Tests for ERP5 software release Tests for Slapos Master software release
...@@ -25,15 +25,24 @@ ...@@ -25,15 +25,24 @@
# #
############################################################################## ##############################################################################
import json
import os import os
import unittest
import logging
if os.environ.get('DEBUG'): from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
raise ValueError("Don't set DEBUG - it breaks postfix compilation - set SLAPOS_TEST_DEBUG instead.")
debug_mode = os.environ.get('SLAPOS_TEST_DEBUG') setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
# for development: debugging logs and install Ctrl+C handler os.path.abspath(
if debug_mode: os.path.join(os.path.dirname(__file__), '..', '..', 'software.cfg')))
logging.basicConfig(level=logging.DEBUG)
unittest.installHandler()
class ERP5InstanceTestCase(SlapOSInstanceTestCase):
"""ERP5 base test case
"""
# ERP5 instanciation needs to run several times before being ready, as
# the root instance request more instances.
instance_max_retry = 7 # XXX how many times ?
def getRootPartitionConnectionParameterDict(self):
"""Return the output paramters from the root partition"""
return json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
...@@ -29,26 +29,15 @@ import os ...@@ -29,26 +29,15 @@ import os
import json import json
import glob import glob
import urlparse import urlparse
import logging import socket
import time import time
import psutil
import requests import requests
from utils import SlapOSInstanceTestCase from . import ERP5InstanceTestCase
from . import setUpModule
setUpModule # pyflakes
class ERP5TestCase(SlapOSInstanceTestCase):
"""Test the remote driver on a minimal web server.
"""
logger = logging.getLogger(__name__)
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'software.cfg')), )
def getRootPartitionConnectionParameterDict(self):
"""Return the output paramters from the root partition"""
return json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
class TestPublishedURLIsReachableMixin(object): class TestPublishedURLIsReachableMixin(object):
...@@ -61,9 +50,10 @@ class TestPublishedURLIsReachableMixin(object): ...@@ -61,9 +50,10 @@ class TestPublishedURLIsReachableMixin(object):
# with 503 Service Unavailable. # with 503 Service Unavailable.
# If we can move the "create site" in slapos node instance, then this retry loop # If we can move the "create site" in slapos node instance, then this retry loop
# would not be necessary. # would not be necessary.
for i in range(1, 20): for i in range(1, 60):
r = requests.get(url, verify=False) # XXX can we get CA from caucase already ? r = requests.get(url, verify=False) # XXX can we get CA from caucase already ?
if r.status_code == requests.codes.service_unavailable: if r.status_code in (requests.codes.service_unavailable,
requests.codes.not_found):
delay = i * 2 delay = i * 2
self.logger.warn("ERP5 was not available, sleeping for %ds and retrying", delay) self.logger.warn("ERP5 was not available, sleeping for %ds and retrying", delay)
time.sleep(delay) time.sleep(delay)
...@@ -89,13 +79,13 @@ class TestPublishedURLIsReachableMixin(object): ...@@ -89,13 +79,13 @@ class TestPublishedURLIsReachableMixin(object):
urlparse.urljoin(param_dict['family-default'], param_dict['site-id'])) urlparse.urljoin(param_dict['family-default'], param_dict['site-id']))
class TestDefaultParameters(ERP5TestCase, TestPublishedURLIsReachableMixin): class TestDefaultParameters(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 can be instanciated with no parameters """Test ERP5 can be instanciated with no parameters
""" """
__partition_reference__ = 'defp' __partition_reference__ = 'defp'
class TestDisableTestRunner(ERP5TestCase, TestPublishedURLIsReachableMixin): class TestDisableTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 can be instanciated without test runner. """Test ERP5 can be instanciated without test runner.
""" """
__partition_reference__ = 'distr' __partition_reference__ = 'distr'
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the `slapos.core.XXX` computer partition instance.
* `computer_partition_root_path`: the path of the instance root directory,
A note about paths:
SlapOS itself and some services running in SlapOS uses unix sockets and (sometimes very)
deep path, which does not play very well together. To workaround this, users can
set SLAPOS_TEST_WORKING_DIR enivonment variable to the path of a short enough directory
and local slapos will be in this directory.
The partitions references will be named after the unittest class name, which can also lead
to long paths. For this, unit test classes can define a __partition_reference__ attribute
which will be used as partition reference. The trick is then to use a shorter
__partition_reference__
See https://lab.nexedi.com/kirr/slapns for the solution to all these problems.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except BaseException:
cls.stopSlapOSProcesses()
cls.setUp = lambda self: self.fail('Setup Class failed.')
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': getattr(cls, '__partition_reference__', cls.__name__),
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -112,6 +112,7 @@ eggs = ...@@ -112,6 +112,7 @@ eggs =
${bcrypt:egg} ${bcrypt:egg}
slapos.libnetworkcache slapos.libnetworkcache
slapos.core slapos.core
supervisor
${slapos.cookbook-setup:egg} ${slapos.cookbook-setup:egg}
${slapos.test.caddy-frontend-setup:egg} ${slapos.test.caddy-frontend-setup:egg}
${slapos.test.erp5-setup:egg} ${slapos.test.erp5-setup:egg}
...@@ -128,12 +129,13 @@ eggs = ...@@ -128,12 +129,13 @@ eggs =
${slapos.test.nextcloud-setup:egg} ${slapos.test.nextcloud-setup:egg}
${slapos.test.turnserver-setup:egg} ${slapos.test.turnserver-setup:egg}
${backports.lzma:egg} ${backports.lzma:egg}
entry-points = entry-points =
runTestSuite=erp5.util.testsuite:runTestSuite runTestSuite=erp5.util.testsuite:runTestSuite
scripts = scripts =
runTestSuite runTestSuite
slapos slapos
supervisorctl
supervisord
interpreter= interpreter=
python_for_test python_for_test
......
...@@ -15,5 +15,5 @@ ...@@ -15,5 +15,5 @@
[template] [template]
filename = instance.cfg filename = instance.cfg
md5sum = c7becd2a66b3305406f965b969907d52 md5sum = b48db2861644d9e9c30d2652c100d70e
...@@ -84,7 +84,7 @@ command-line = ...@@ -84,7 +84,7 @@ command-line =
# guarantee about free ports on IPV4 and IPV6 # guarantee about free ports on IPV4 and IPV6
# * LOCAL_IPV4 is backward compatible, to be migrated, SLAPOS_TEST_IPV4 # * LOCAL_IPV4 is backward compatible, to be migrated, SLAPOS_TEST_IPV4
environment = environment =
PATH=${coreutils:location}/bin:${curl:location}/bin:${openssl:location}/bin:${git:location}/bin:${libxslt:location}/bin:${socat:location}/bin:${lmsensors:location}/bin:/usr/bin/:/bin/ PATH=${coreutils:location}/bin:${curl:location}/bin:${openssl:location}/bin:${git:location}/bin:${libxslt:location}/bin:${socat:location}/bin:${lmsensors:location}/bin:${buildout:bin-directory}:/usr/bin/:/bin/
LOCAL_IPV4=$${slap-configuration:ipv4-random} LOCAL_IPV4=$${slap-configuration:ipv4-random}
SLAPOS_TEST_IPV4=$${slap-configuration:ipv4-random} SLAPOS_TEST_IPV4=$${slap-configuration:ipv4-random}
SLAPOS_TEST_IPV6=$${slap-configuration:ipv6-random} SLAPOS_TEST_IPV6=$${slap-configuration:ipv6-random}
......
...@@ -114,10 +114,13 @@ eggs = ...@@ -114,10 +114,13 @@ eggs =
zope.testing zope.testing
httmock httmock
pyflakes pyflakes
supervisor
entry-points = entry-points =
runTestSuite=erp5.util.testsuite:runTestSuite runTestSuite=erp5.util.testsuite:runTestSuite
scripts = scripts =
runTestSuite runTestSuite
slapos
supervisord
interpreter= interpreter=
python_for_test python_for_test
...@@ -182,11 +185,14 @@ output = ${buildout:directory}/template.cfg ...@@ -182,11 +185,14 @@ output = ${buildout:directory}/template.cfg
mode = 640 mode = 640
[versions] [versions]
Pygments = 2.1.3 # clear version pins of tested eggs for which we want to generate scripts
# otherwise, the scripts will be generated for the pinned version.
slapos.recipe.template = 4.3 slapos.core =
erp5.util =
# All depencies should be pinned. # All depencies should be pinned.
Pygments = 2.1.3
slapos.recipe.template = 4.3
zc.lockfile = 1.4 zc.lockfile = 1.4
bcrypt = 3.1.4 bcrypt = 3.1.4
dnspython = 1.15.0 dnspython = 1.15.0
......
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"properties": { "properties": {
"instance-name": {
"title": "Instance Name",
"description": "Name of the instance, to show in the window title",
"type": "string"
},
"custom-frontend-backend-url": {
"title": "Custom Frontend Backend URL",
"description": "return an ipv4 frontend of the given ipv6(+optional port)",
"type": "string",
"format": "uri"
},
"custom-frontend-backend-type": {
"title": "Custom Frontend Backend Type",
"description": "The type of the frontend slave instance to ask",
"type": "string",
"enum": [
"zope"
]
},
"user-authorized-key": { "user-authorized-key": {
"title": "User Authorized Key", "title": "User Authorized Key",
"description": "SSH public key in order to connect to the SSH server of this runner instance.", "description": "SSH public key in order to connect to the SSH server of this runner instance.",
...@@ -14,16 +33,26 @@ ...@@ -14,16 +33,26 @@
"minimum": 1, "minimum": 1,
"maximum": 40 "maximum": 40
}, },
"slapos-software": {
"title": "Pre-selected Software Release",
"description": "a relative path from the slapos git repo to a folder containing a software release, which will be automaticaly deployed while the runner instanciation, and only if the parameter auto-deploy is set to 'true'. For example: 'software/helloworld",
"type": "string"
},
"auto-deploy": { "auto-deploy": {
"title": "Automatically Deploy Software", "title": "Automatically Deploy Software",
"description": "Authorizes the software declared with 'slapos-software' to be automatically deployed, or not. Needs instance to be restarted. (default is false)", "description": "Authorizes the software declared with 'slapos-software' to be automatically deployed, or not. Needs instance to be restarted. (default is false)",
"type": "boolean" "type": "boolean"
}, },
"auto-deploy-instance": {
"title": "Automatically Deploy Instances",
"description": "Prevent the runner from deploying and starting instances. Needs instance to be restarted. It is set to false for instances of type 'import' in resiliency in any case (default is false)",
"type": "boolean"
},
"autorun": {
"title": "Automatically Run Sofware/Instance",
"description": "Let automaticaly build and run a declared software with 'slapos-software'. Only works if 'slapos-software' is set, and 'auto-deploy' is true. Needs instance to be restarted. (default is false)",
"type": "boolean"
},
"slapos-software": {
"title": "Pre-selected Software Release",
"description": "a relative path from the slapos git repo to a folder containing a software release, which will be automaticaly deployed while the runner instanciation, and only if the parameter auto-deploy is set to 'true'. For example: 'software/helloworld",
"type": "string"
},
"slapos-repository": { "slapos-repository": {
"title": "SlapOS Git Repository URL", "title": "SlapOS Git Repository URL",
"description": "url of the default git repository that will be download by the runner while its instanciation. Will be cloned in a directory named 'slapos' (default is https://lab.nexedi.com/nexedi/slapos.git)", "description": "url of the default git repository that will be download by the runner while its instanciation. Will be cloned in a directory named 'slapos' (default is https://lab.nexedi.com/nexedi/slapos.git)",
...@@ -36,16 +65,6 @@ ...@@ -36,16 +65,6 @@
"description": "Branch or hash on which the default repository will checkout (default is master)", "description": "Branch or hash on which the default repository will checkout (default is master)",
"type": "string" "type": "string"
}, },
"auto-deploy-instance": {
"title": "Automatically Deploy Instances",
"description": "Prevent the runner from deploying and starting instances. Needs instance to be restarted. It is set to false for instances of type 'import' in resiliency in any case (default is false)",
"type": "boolean"
},
"autorun": {
"title": "Automatically Run Sofware/Instance",
"description": "Let automaticaly build and run a declared software with 'slapos-software'. Only works if 'slapos-software' is set, and 'auto-deploy' is true. Needs instance to be restarted. (default is false)",
"type": "boolean"
},
"slapos-software-type": { "slapos-software-type": {
"title": "Deployed Instance Software Type", "title": "Deployed Instance Software Type",
"description": "Software type of your instance inside the runner", "description": "Software type of your instance inside the runner",
...@@ -66,20 +85,6 @@ ...@@ -66,20 +85,6 @@
], ],
"default": "false" "default": "false"
}, },
"custom-frontend-backend-url": {
"title": "Custom Frontend Backend URL",
"description": "return an ipv4 frontend of the given ipv6(+optional port)",
"type": "string",
"format": "uri"
},
"custom-frontend-backend-type": {
"title": "Custom Frontend Backend Type",
"description": "The type of the frontend slave instance to ask",
"type": "string",
"enum": [
"zope"
]
},
"custom-frontend-basic-auth": { "custom-frontend-basic-auth": {
"title": "Custom Frontend Basic Auth", "title": "Custom Frontend Basic Auth",
"description": "if the ip given with 'custom-frontend-backend-url' is secure, set it to true for the promise do not fail", "description": "if the ip given with 'custom-frontend-backend-url' is secure, set it to true for the promise do not fail",
...@@ -139,11 +144,6 @@ ...@@ -139,11 +144,6 @@
"description": "List of cors domains separated with space. Needed for ajax query on this monitor instance from a different domain.", "description": "List of cors domains separated with space. Needed for ajax query on this monitor instance from a different domain.",
"type": "string", "type": "string",
"default": "monitor.app.officejs.com" "default": "monitor.app.officejs.com"
},
"instance-name": {
"title": "Instance Name",
"description": "Name of the instance, to show in the window title",
"type": "string"
} }
} }
} }
...@@ -25,12 +25,11 @@ ...@@ -25,12 +25,11 @@
# #
############################################################################## ##############################################################################
from setuptools import setup, find_packages from setuptools import setup, find_packages
import glob
import os
version = '0.0.1.dev0' version = '0.0.1.dev0'
name = 'slapos.test.slaprunner' name = 'slapos.test.slaprunner'
long_description = open("README.md").read() with open("README.md") as f:
long_description = f.read()
setup(name=name, setup(name=name,
version=version, version=version,
......
...@@ -26,35 +26,21 @@ ...@@ -26,35 +26,21 @@
############################################################################## ##############################################################################
import os import os
import shutil
import urlparse
import tempfile
import requests
import socket
import StringIO
import subprocess
import json
import psutil
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
if os.environ.get('SLAPOS_TEST_DEBUG'): os.path.abspath(
import logging os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
class InstanceTestCase(utils.SlapOSInstanceTestCase): class SlaprunnerTestCase(SlapOSInstanceTestCase):
@classmethod # Slaprunner uses unix sockets, so it needs short paths.
def getSoftwareURLList(cls): __partition_reference__ = 's'
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class ServicesTestCase(InstanceTestCase):
class ServicesTestCase(SlaprunnerTestCase):
def test_hashes(self): def test_hashes(self):
hash_files = [ hash_files = [
'software_release/buildout.cfg', 'software_release/buildout.cfg',
...@@ -71,9 +57,10 @@ class ServicesTestCase(InstanceTestCase): ...@@ -71,9 +57,10 @@ class ServicesTestCase(InstanceTestCase):
'supervisord-{hash}-on-watch', 'supervisord-{hash}-on-watch',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name'] process_names = [
for process in supervisor.getAllProcessInfo()] process['name'] for process in supervisor.getAllProcessInfo()
]
hash_files = [os.path.join(self.computer_partition_root_path, path) hash_files = [os.path.join(self.computer_partition_root_path, path)
for path in hash_files] for path in hash_files]
......
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
# THIS IS NOT A BUILDOUT FILE, despite purposedly using a compatible syntax.
# The only allowed lines here are (regexes):
# - "^#" comments, copied verbatim
# - "^[" section beginings, copied verbatim
# - lines containing an "=" sign which must fit in the following categorie.
# - "^\s*filename\s*=\s*path\s*$" where "path" is relative to this file
# Copied verbatim.
# - "^\s*hashtype\s*=.*" where "hashtype" is one of the values supported
# by the re-generation script.
# Re-generated.
# - other lines are copied verbatim
# Substitution (${...:...}), extension ([buildout] extends = ...) and
# section inheritance (< = ...) are NOT supported (but you should really
# not need these here).
[instance-cfg]
filename = instance.cfg.in
md5sum = d027a2dccaf15ae6e7d3a28cc02d70c3
[template-turnserver]
filename = instance-turnserver.cfg.jinja2.in
md5sum = 6ba54fb299e1fd59617e5a6a9545e36e
...@@ -109,8 +109,12 @@ mode = 644 ...@@ -109,8 +109,12 @@ mode = 644
[turnserver-wrapper] [turnserver-wrapper]
recipe = slapos.cookbook:wrapper recipe = slapos.cookbook:wrapper
command-line = {{ parameter_dict['turnserver-location'] }}/bin/turnserver # XXX on first invocation of read-secret, the secret file is not yet generated
-c ${turnserver-config:output} # so on first buildout run turnserver-config has an empty secret.
# We don't want to start the server when config file is not complete.
command-line =
bash -c "egrep static-auth-secret=.+ ${turnserver-config:output} && \
{{ parameter_dict['turnserver-location'] }}/bin/turnserver -c ${turnserver-config:output}"
wrapper-path = ${directory:services}/turnserver wrapper-path = ${directory:services}/turnserver
hash-existing-files = ${buildout:directory}/software_release/buildout.cfg hash-existing-files = ${buildout:directory}/software_release/buildout.cfg
......
...@@ -7,6 +7,7 @@ extends = ...@@ -7,6 +7,7 @@ extends =
../../component/socat/buildout.cfg ../../component/socat/buildout.cfg
../../stack/monitor/buildout.cfg ../../stack/monitor/buildout.cfg
../../stack/slapos.cfg ../../stack/slapos.cfg
buildout.hash.cfg
parts += parts +=
...@@ -22,14 +23,11 @@ mode = 644 ...@@ -22,14 +23,11 @@ mode = 644
[instance-cfg] [instance-cfg]
recipe = slapos.recipe.template recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance.cfg.in url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/instance.cfg output = ${buildout:directory}/instance.cfg
md5sum = d027a2dccaf15ae6e7d3a28cc02d70c3
[template-turnserver] [template-turnserver]
<= download-base <= download-base
filename = instance-turnserver.cfg.jinja2.in
md5sum = f275df4900a9db1d1a72b67b12ee8afe
[versions] [versions]
slapos.recipe.template = 4.3 slapos.recipe.template = 4.3
...@@ -31,33 +31,34 @@ import json ...@@ -31,33 +31,34 @@ import json
import glob import glob
import ConfigParser import ConfigParser
import utils
from slapos.recipe.librecipe import generateHashFromFiles from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
# for development: debugging logs and install Ctrl+C handler
if os.environ.get('SLAPOS_TEST_DEBUG'):
import logging
logging.basicConfig(level=logging.DEBUG)
import unittest
unittest.installHandler()
def subprocess_status_output(*args, **kwargs): setUpModule, InstanceTestCase = makeModuleSetUpAndTestCaseClass(
prc = subprocess.Popen( os.path.abspath(
stdout=subprocess.PIPE, os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
stderr=subprocess.STDOUT,
*args,
**kwargs)
out, err = prc.communicate()
return prc.returncode, out
class InstanceTestCase(utils.SlapOSInstanceTestCase):
@classmethod
def getSoftwareURLList(cls):
return (os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'software.cfg')), )
class TurnServerTestCase(InstanceTestCase):
partition_path = None
def setUp(self):
# Lookup the partition in which turnserver was installed.
partition_path_list = glob.glob(os.path.join(
self.slap.instance_directory, '*'))
for partition_path in partition_path_list:
if os.path.exists(os.path.join(partition_path, 'etc/turnserver.conf')):
self.partition_path = partition_path
break
self.assertTrue(
self.partition_path,
"Turnserver path not found in %r" % (partition_path_list,))
class ServicesTestCase(InstanceTestCase): class TestServices(TurnServerTestCase):
def test_process_list(self): def test_process_list(self):
hash_list = [ hash_list = [
...@@ -72,8 +73,8 @@ class ServicesTestCase(InstanceTestCase): ...@@ -72,8 +73,8 @@ class ServicesTestCase(InstanceTestCase):
'monitor-httpd-graceful', 'monitor-httpd-graceful',
] ]
supervisor = self.getSupervisorRPCServer().supervisor with self.slap.instance_supervisor_rpc as supervisor:
process_name_list = [process['name'] process_name_list = [process['name']
for process in supervisor.getAllProcessInfo()] for process in supervisor.getAllProcessInfo()]
hash_file_list = [os.path.join(self.computer_partition_root_path, path) hash_file_list = [os.path.join(self.computer_partition_root_path, path)
...@@ -86,19 +87,14 @@ class ServicesTestCase(InstanceTestCase): ...@@ -86,19 +87,14 @@ class ServicesTestCase(InstanceTestCase):
self.assertIn(expected_process_name, process_name_list) self.assertIn(expected_process_name, process_name_list)
def test_default_deployment(self): def test_default_deployment(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*')) secret_file = os.path.join(self.partition_path, 'etc/.turnsecret')
instance_folder = None self.assertTrue(os.path.exists(self.partition_path))
for partition_path in partition_path_list:
if os.path.exists(os.path.join(partition_path, 'etc/turnserver.conf')):
instance_folder = partition_path
break
secret_file = os.path.join(instance_folder, 'etc/.turnsecret')
self.assertTrue(os.path.exists(instance_folder))
self.assertTrue(os.path.exists(secret_file)) self.assertTrue(os.path.exists(secret_file))
config = ConfigParser.ConfigParser() config = ConfigParser.ConfigParser()
config.readfp(open(secret_file)) with open(secret_file) as f:
config.readfp(f)
secret = config.get('turnserver', 'secret') secret = config.get('turnserver', 'secret')
self.assertTrue(secret)
expected_config = """listening-port=3478 expected_config = """listening-port=3478
tls-listening-port=5349 tls-listening-port=5349
...@@ -125,70 +121,15 @@ no-stdout-log ...@@ -125,70 +121,15 @@ no-stdout-log
log-file=%(instance_path)s/var/log/turnserver.log log-file=%(instance_path)s/var/log/turnserver.log
userdb=%(instance_path)s/srv/turndb userdb=%(instance_path)s/srv/turndb
pidfile=%(instance_path)s/var/run/turnserver.pid pidfile=%(instance_path)s/var/run/turnserver.pid
verbose""" % {'instance_path': instance_folder, 'secret': secret, 'ipv4': self.config['ipv4_address']} verbose""" % {'instance_path': self.partition_path, 'secret': secret, 'ipv4': self._ipv4_address}
with open(os.path.join(instance_folder, 'etc/turnserver.conf')) as f: with open(os.path.join(self.partition_path, 'etc/turnserver.conf')) as f:
current_config = f.read().strip() current_config = f.read().strip()
self.assertEqual(current_config, expected_config) self.assertEqual(current_config.splitlines(), expected_config.splitlines())
def test_turnserver_promises(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*'))
instance_folder = None
for partition_path in partition_path_list:
if os.path.exists(os.path.join(partition_path, 'etc/turnserver.conf')):
instance_folder = partition_path
break
self.assertTrue(os.path.exists(instance_folder))
promise_path_list = glob.glob(os.path.join(instance_folder, 'etc/plugin/*.py'))
promise_name_list = [x for x in
os.listdir(os.path.join(instance_folder, 'etc/plugin'))
if not x.endswith('.pyc')]
partition_name = os.path.basename(instance_folder.rstrip('/'))
self.assertEqual(sorted(promise_name_list),
sorted([
"__init__.py",
"check-free-disk-space.py",
"monitor-http-frontend.py",
"buildout-%s-status.py" % partition_name,
"monitor-bootstrap-status.py",
"monitor-httpd-listening-on-tcp.py",
"turnserver-port-listening.py",
"turnserver-tls-port-listening.py",
]))
ignored_plugin_list = [ class TestParameters(TurnServerTestCase):
'__init__.py',
'monitor-http-frontend.py',
]
runpromise_bin = os.path.join(
self.software_path, 'bin', 'monitor.runpromise')
monitor_conf = os.path.join(instance_folder, 'etc', 'monitor.conf')
msg = []
status = 0
for plugin_path in promise_path_list:
plugin_name = os.path.basename(plugin_path)
if plugin_name in ignored_plugin_list:
continue
plugin_status, plugin_result = subprocess_status_output([
runpromise_bin,
'-c', monitor_conf,
'--run-only', plugin_name,
'--force',
'--check-anomaly'
])
status += plugin_status
if plugin_status == 1:
msg.append(plugin_result)
# sanity check
if 'Checking promise %s' % plugin_name not in plugin_result:
plugin_status = 1
msg.append(plugin_result)
msg = ''.join(msg).strip()
self.assertEqual(status, 0, msg)
class ParametersTestCase(InstanceTestCase):
@classmethod @classmethod
def getInstanceParameterDict(cls): def getInstanceParameterDict(cls):
return { return {
...@@ -200,19 +141,14 @@ class ParametersTestCase(InstanceTestCase): ...@@ -200,19 +141,14 @@ class ParametersTestCase(InstanceTestCase):
} }
def test_turnserver_with_parameters(self): def test_turnserver_with_parameters(self):
partition_path_list = glob.glob(os.path.join(self.instance_path, '*')) secret_file = os.path.join(self.partition_path, 'etc/.turnsecret')
instance_folder = None self.assertTrue(os.path.exists(self.partition_path))
for partition_path in partition_path_list:
if os.path.exists(os.path.join(partition_path, 'etc/turnserver.conf')):
instance_folder = partition_path
break
secret_file = os.path.join(instance_folder, 'etc/.turnsecret')
self.assertTrue(os.path.exists(instance_folder))
self.assertTrue(os.path.exists(secret_file)) self.assertTrue(os.path.exists(secret_file))
config = ConfigParser.ConfigParser() config = ConfigParser.ConfigParser()
config.readfp(open(secret_file)) with open(secret_file) as f:
config.readfp(f)
secret = config.get('turnserver', 'secret') secret = config.get('turnserver', 'secret')
self.assertTrue(secret)
expected_config = """listening-port=%(port)s expected_config = """listening-port=%(port)s
tls-listening-port=%(tls_port)s tls-listening-port=%(tls_port)s
...@@ -240,7 +176,7 @@ no-stdout-log ...@@ -240,7 +176,7 @@ no-stdout-log
log-file=%(instance_path)s/var/log/turnserver.log log-file=%(instance_path)s/var/log/turnserver.log
userdb=%(instance_path)s/srv/turndb userdb=%(instance_path)s/srv/turndb
pidfile=%(instance_path)s/var/run/turnserver.pid pidfile=%(instance_path)s/var/run/turnserver.pid
verbose""" % {'instance_path': instance_folder, verbose""" % {'instance_path': self.partition_path,
'secret': secret, 'secret': secret,
'ipv4': '127.0.0.1', 'ipv4': '127.0.0.1',
'name': 'turn.site.com', 'name': 'turn.site.com',
...@@ -248,9 +184,7 @@ verbose""" % {'instance_path': instance_folder, ...@@ -248,9 +184,7 @@ verbose""" % {'instance_path': instance_folder,
'port': 3488, 'port': 3488,
'tls_port': 5369,} 'tls_port': 5369,}
with open(os.path.join(instance_folder, 'etc/turnserver.conf')) as f: with open(os.path.join(self.partition_path, 'etc/turnserver.conf')) as f:
current_config = f.read().strip() current_config = f.read().strip()
self.assertEqual(current_config, expected_config) self.assertEqual(current_config.splitlines(), expected_config.splitlines())
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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 socket
from contextlib import closing
import logging
import StringIO
import xmlrpclib
import supervisor.xmlrpc
from erp5.util.testnode.SlapOSControler import SlapOSControler
from erp5.util.testnode.ProcessManager import ProcessManager
# Utility functions
def findFreeTCPPort(ip=''):
"""Find a free TCP port to listen to.
"""
family = socket.AF_INET6 if ':' in ip else socket.AF_INET
with closing(socket.socket(family, socket.SOCK_STREAM)) as s:
s.bind((ip, 0))
return s.getsockname()[1]
# TODO:
# - allow requesting multiple instances ?
class SlapOSInstanceTestCase(unittest.TestCase):
"""Install one slapos instance.
This test case install software(s) and request one instance during `setUpClass`
and destroy the instance during `tearDownClass`.
Software Release URL, Instance Software Type and Instance Parameters can be defined
on the class.
All tests from the test class will run with the same instance.
The following class attributes are available:
* `computer_partition`: the computer partition instance, implementing
`slapos.slap.interface.slap.IComputerPartition`.
* `computer_partition_root_path`: the path of the instance root directory.
"""
# Methods to be defined by subclasses.
@classmethod
def getSoftwareURLList(cls):
"""Return URL of software releases to install.
To be defined by subclasses.
"""
raise NotImplementedError()
@classmethod
def getInstanceParameterDict(cls):
"""Return instance parameters
To be defined by subclasses if they need to request instance with specific
parameters.
"""
return {}
@classmethod
def getInstanceSoftwareType(cls):
"""Return software type for instance, default "default"
To be defined by subclasses if they need to request instance with specific
software type.
"""
return "default"
# Utility methods.
def getSupervisorRPCServer(self):
"""Returns a XML-RPC connection to the supervisor used by slapos node
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-supervisor',
transport=supervisor.xmlrpc.SupervisorTransport(
None,
None,
# XXX hardcoded socket path
serverurl="unix://{working_directory}/inst/supervisord.socket".format(
**self.config)))
# Unittest methods
@classmethod
def setUpClass(cls):
"""Setup the class, build software and request an instance.
If you have to override this method, do not forget to call this method on
parent class.
"""
try:
cls.setUpWorkingDirectory()
cls.setUpConfig()
cls.setUpSlapOSController()
cls.runSoftwareRelease()
# XXX instead of "runSoftwareRelease", it would be better to be closer to slapos usage:
# cls.supplySoftwares()
# cls.installSoftwares()
cls.runComputerPartition()
# XXX instead of "runComputerPartition", it would be better to be closer to slapos usage:
# cls.requestInstances()
# cls.createInstances()
# cls.requestInstances()
except Exception:
cls.stopSlapOSProcesses()
raise
cls.instance_path = os.path.join(
cls.config['working_directory'],
'inst')
cls.software_path = os.path.realpath(os.path.join(
cls.computer_partition_root_path, 'software_release'))
@classmethod
def tearDownClass(cls):
"""Tear down class, stop the processes and destroy instance.
"""
cls.stopSlapOSProcesses()
# Implementation
@classmethod
def stopSlapOSProcesses(cls):
if hasattr(cls, '_process_manager'):
cls._process_manager.killPreviousRun()
@classmethod
def setUpWorkingDirectory(cls):
"""Initialise the directories"""
cls.working_directory = os.environ.get(
'SLAPOS_TEST_WORKING_DIR',
os.path.join(os.path.dirname(__file__), '.slapos'))
# 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(cls.working_directory + '/inst/supervisord.socket.xxxxxxx') > 108:
raise RuntimeError('working directory ( {} ) is too deep, try setting '
'SLAPOS_TEST_WORKING_DIR'.format(cls.working_directory))
if not os.path.exists(cls.working_directory):
os.mkdir(cls.working_directory)
@classmethod
def setUpConfig(cls):
"""Create slapos configuration"""
cls.config = {
"working_directory": cls.working_directory,
"slapos_directory": cls.working_directory,
"log_directory": cls.working_directory,
"computer_id": 'slapos.test', # XXX
'proxy_database': os.path.join(cls.working_directory, 'proxy.db'),
'partition_reference': cls.__name__,
# "proper" slapos command must be in $PATH
'slapos_binary': 'slapos',
'node_quantity': '3',
}
# Some tests are expecting that local IP is not set to 127.0.0.1
ipv4_address = os.environ.get('SLAPOS_TEST_IPV4', '127.0.1.1')
ipv6_address = os.environ['SLAPOS_TEST_IPV6']
cls.config['proxy_host'] = cls.config['ipv4_address'] = ipv4_address
cls.config['ipv6_address'] = ipv6_address
cls.config['proxy_port'] = findFreeTCPPort(ipv4_address)
cls.config['master_url'] = 'http://{proxy_host}:{proxy_port}'.format(
**cls.config)
@classmethod
def setUpSlapOSController(cls):
"""Create the a "slapos controller" and supply softwares from `getSoftwareURLList`.
This is equivalent to:
slapos proxy start
for sr in getSoftwareURLList; do
slapos supply $SR $COMP
done
"""
cls._process_manager = ProcessManager()
# XXX this code is copied from testnode code
cls.slapos_controler = SlapOSControler(
cls.working_directory,
cls.config
)
slapproxy_log = os.path.join(cls.config['log_directory'], 'slapproxy.log')
logger = logging.getLogger(__name__)
logger.debug('Configured slapproxy log to %r', slapproxy_log)
cls.software_url_list = cls.getSoftwareURLList()
cls.slapos_controler.initializeSlapOSControler(
slapproxy_log=slapproxy_log,
process_manager=cls._process_manager,
reset_software=False,
software_path_list=cls.software_url_list)
# XXX we should check *earlier* if that pidfile exist and if supervisord
# process still running, because if developer started supervisord (or bugs?)
# then another supervisord will start and starting services a second time
# will fail.
cls._process_manager.supervisord_pid_file = os.path.join(
cls.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
@classmethod
def runSoftwareRelease(cls):
"""Run all the software releases that were supplied before.
This is the equivalent of `slapos node software`.
The tests will be marked file if software building fail.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
try:
cls.software_status_dict = cls.slapos_controler.runSoftwareRelease(
cls.config, environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.software_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
@classmethod
def runComputerPartition(cls):
"""Instanciate the software.
This is the equivalent of doing:
slapos request --type=getInstanceSoftwareType --parameters=getInstanceParameterDict
slapos node instance
and return the slapos request instance parameters.
This can be called by tests to simulate re-request with different parameters.
"""
logger = logging.getLogger()
logger.level = logging.DEBUG
stream = StringIO.StringIO()
stream_handler = logging.StreamHandler(stream)
logger.addHandler(stream_handler)
if cls.getInstanceSoftwareType() != 'default':
raise NotImplementedError
instance_parameter_dict = cls.getInstanceParameterDict()
try:
cls.instance_status_dict = cls.slapos_controler.runComputerPartition(
cls.config,
cluster_configuration=instance_parameter_dict,
environment=os.environ)
stream.seek(0)
stream.flush()
message = ''.join(stream.readlines()[-100:])
assert cls.instance_status_dict['status_code'] == 0, message
finally:
logger.removeHandler(stream_handler)
del stream
# FIXME: similar to test node, only one (root) partition is really
# supported for now.
computer_partition_list = []
for i in range(len(cls.software_url_list)):
computer_partition_list.append(
cls.slapos_controler.slap.registerOpenOrder().request(
cls.software_url_list[i],
# This is how testnode's SlapOSControler name created partitions
partition_reference='testing partition {i}'.format(
i=i, **cls.config),
partition_parameter_kw=instance_parameter_dict))
# expose some class attributes so that tests can use them:
# the ComputerPartition instances, to getInstanceParameterDict
cls.computer_partition = computer_partition_list[0]
# the path of the instance on the filesystem, for low level inspection
cls.computer_partition_root_path = os.path.join(
cls.config['working_directory'],
'inst',
cls.computer_partition.getId())
...@@ -137,13 +137,13 @@ pytz = 2016.10 ...@@ -137,13 +137,13 @@ pytz = 2016.10
requests = 2.13.0 requests = 2.13.0
six = 1.12.0 six = 1.12.0
slapos.cookbook = 1.0.123 slapos.cookbook = 1.0.123
slapos.core = 1.4.28 slapos.core = 1.5.0
slapos.extension.strip = 0.4 slapos.extension.strip = 0.4
slapos.extension.shared = 1.0 slapos.extension.shared = 1.0
slapos.libnetworkcache = 0.19 slapos.libnetworkcache = 0.19
slapos.rebootstrap = 4.1 slapos.rebootstrap = 4.2
slapos.recipe.build = 0.41 slapos.recipe.build = 0.41
slapos.recipe.cmmi = 0.10 slapos.recipe.cmmi = 0.11
slapos.toolbox = 0.95 slapos.toolbox = 0.95
stevedore = 1.21.0 stevedore = 1.21.0
subprocess32 = 3.5.3 subprocess32 = 3.5.3
...@@ -154,7 +154,7 @@ CacheControl = 0.12.5 ...@@ -154,7 +154,7 @@ CacheControl = 0.12.5
msgpack = 0.6.1 msgpack = 0.6.1
# Required by: # Required by:
# slapos.core==1.4.26 # slapos.core==1.5.0
Flask = 0.12 Flask = 0.12
# Required by: # Required by:
...@@ -195,7 +195,7 @@ enum34 = 1.1.6 ...@@ -195,7 +195,7 @@ enum34 = 1.1.6
# Required by: # Required by:
# slapos.toolbox==0.94 # slapos.toolbox==0.94
erp5.util = 0.4.62 erp5.util = 0.4.64
# Required by: # Required by:
# slapos.toolbox==0.94 # slapos.toolbox==0.94
...@@ -226,7 +226,7 @@ jsonschema = 3.0.2 ...@@ -226,7 +226,7 @@ jsonschema = 3.0.2
lockfile = 0.12.2 lockfile = 0.12.2
# Required by: # Required by:
# slapos.core==1.4.26 # slapos.core==1.5.0
# XXX 'slapos node format' raises an exception with netifaces 0.10.5. # XXX 'slapos node format' raises an exception with netifaces 0.10.5.
netifaces = 0.10.7 netifaces = 0.10.7
...@@ -259,7 +259,7 @@ python-dateutil = 2.7.3 ...@@ -259,7 +259,7 @@ python-dateutil = 2.7.3
rpdb = 0.1.5 rpdb = 0.1.5
# Required by: # Required by:
# slapos.core==1.4.26 # slapos.core==1.5.0
supervisor = 3.3.3 supervisor = 3.3.3
# Required by: # Required by:
...@@ -267,11 +267,11 @@ supervisor = 3.3.3 ...@@ -267,11 +267,11 @@ supervisor = 3.3.3
tzlocal = 1.5.1 tzlocal = 1.5.1
# Required by: # Required by:
# slapos.core==1.4.26 # slapos.core==1.5.0
uritemplate = 3.0.0 uritemplate = 3.0.0
# Required by: # Required by:
# slapos.core==1.4.26 # slapos.core==1.5.0
zope.interface = 4.3.3 zope.interface = 4.3.3
[networkcache] [networkcache]
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment