# -*- coding: utf-8 -*- # vim: set et sts=2: # pylint: disable-msg=W0311,C0301,C0103,C0111,R0904 ############################################# # !!! Attention !!! # You now have to comment the last line # in __init__py, wich starts the functiun # run() in order to start the tests, # or it will NOT work ############################################# from __future__ import print_function import argparse import base64 from six.moves.configparser import SafeConfigParser import datetime import hashlib import json import os import shutil from . import sup_process from io import StringIO import ssl import time import unittest from six.moves.urllib.request import Request, urlopen import six from slapos.runner.utils import (getProfilePath, getSession, isInstanceRunning, isSoftwareRunning, startProxy, isSoftwareReleaseReady, runSlapgridUntilSuccess, runInstanceWithLock, getBuildAndRunParams, saveBuildAndRunParams) from slapos.runner import views import slapos.slap from slapos.htpasswd import HtpasswdFile from .run import Config import erp5.util.taskdistribution as taskdistribution #Helpers def loadJson(response): return json.loads(response.data) def parseArguments(): """ Parse arguments for erp5testnode-backed test. """ parser = argparse.ArgumentParser() # runnertest mandatory arguments parser.add_argument('--key_file', metavar='KEY_FILE', help='Path to the key file used to communicate with slapOS-master') parser.add_argument('--cert_file', metavar='CERT_FILE', help='Path to the cert file used to communicate with slapOS-master') parser.add_argument('--server_url', metavar='SERVER_URL', help='URL of the local slapproxy') parser.add_argument('--computer_id', metavar='COMPUTER_ID', help='ID of the COMP where the slaprunner is running') parser.add_argument('--partition_id', metavar='PARTITION_ID', help='ID of the partition where the slaprunner is depoyed') # Test Node arguments parser.add_argument('--test_result_path', metavar='ERP5_TEST_RESULT_PATH', help='ERP5 relative path of the test result') parser.add_argument('--revision', metavar='REVISION', help='Revision of the test_suite') parser.add_argument('--test_suite', metavar='TEST_SUITE', help='Name of the test suite') parser.add_argument('--test_suite_title', metavar='TEST_SUITE', help='The test suite title') parser.add_argument('--test_node_title', metavar='NODE_TITLE', help='Title of the testnode which is running this' 'launcher') parser.add_argument('--project_title', metavar='PROJECT_TITLE', help='The project title') parser.add_argument('--node_quantity', help='Number of parallel tests to run', default=1, type=int) parser.add_argument('--master_url', metavar='TEST_SUITE_MASTER_URL', help='Url to connect to the ERP5 Master testsuite taskditributor') return parser.parse_args() class SlaprunnerTestCase(unittest.TestCase): @classmethod def setUpClass(cls, **kw): if len(kw) == 0: return cls.server_url = kw['server_url'] cls.key_file = kw['key_file'] cls.cert_file = kw['cert_file'] cls.computer_id = kw['computer_id'] cls.partition_id = kw['partition_id'] # Get parameters returned by slapos master slap = slapos.slap.slap() slap.initializeConnection(cls.server_url, cls.key_file, cls.cert_file) cls.partition = slap.registerComputerPartition( computer_guid=cls.computer_id, partition_id=cls.partition_id ) cls.parameter_dict = cls.partition.getConnectionParameterDict() for attribute, value in six.iteritems(cls.parameter_dict): setattr(cls, attribute.replace('-', '_'), value) #create slaprunner configuration views.app.config['TESTING'] = True config = Config() config.setConfig() views.app.config.update(**config.__dict__) cls.app = views.app.test_client() cls.app.config = views.app.config # Set up path (needed to find git binary) os.environ['PATH'] = config.path def setUp(self): """Initialize slapos webrunner here""" self.users = [self.init_user, self.init_password, "slaprunner@nexedi.com", "SlapOS web runner"] self.updateUser = ["newslapuser", "newslappwd", "slaprunner@nexedi.com", "SlapOS web runner"] self.repo = 'https://lab.nexedi.com/rafael/slapos-workarround.git' self.software = "workspace/slapos/software/" # relative directory fo SR self.project = 'slapos' # Default project name self.template = 'template.cfg' self.partitionPrefix = 'slappart' self.workdir = workdir = os.path.join(self.app.config['runner_workdir'], 'project') software_link = os.path.join(self.app.config['runner_workdir'], 'softwareLink') #update or create all runner base directory to test_dir if not os.path.exists(workdir): os.mkdir(workdir) if not os.path.exists(software_link): os.mkdir(software_link) #Create config.json json_file = os.path.join(views.app.config['etc_dir'], 'config.json') if not os.path.exists(json_file): params = { 'run_instance' : True, 'run_software' : True, 'max_run_instance' : 3, 'max_run_software' : 2 } open(json_file, "w").write(json.dumps(params)) def tearDown(self): """Remove all test data""" project = os.path.join(self.app.config['etc_dir'], '.project') #reset tested parameters self.updateConfigParameter('autorun', False) self.updateConfigParameter('auto_deploy', True) if os.path.exists(project): os.unlink(project) if os.path.exists(self.app.config['workspace']): shutil.rmtree(self.app.config['workspace']) if os.path.exists(self.app.config['software_root']): shutil.rmtree(self.app.config['software_root']) if os.path.exists(self.app.config['instance_root']): shutil.rmtree(self.app.config['instance_root']) if os.path.exists(self.app.config['software_link']): shutil.rmtree(self.app.config['software_link']) def updateConfigParameter(self, parameter, value): config_parser = SafeConfigParser() config_parser.read(os.getenv('RUNNER_CONFIG')) for section in config_parser.sections(): if config_parser.has_option(section, parameter): config_parser.set(section, parameter, str(value)) with open(os.getenv('RUNNER_CONFIG'), 'wb') as configfile: config_parser.write(configfile) def updateAccount(self, newaccount): """Helper for update user account data""" return self.app.post('/updateAccount', data=dict( username=newaccount[0], password=newaccount[1], email=newaccount[2], name=newaccount[3], ), follow_redirects=True) def getCurrentSR(self): return getProfilePath(self.app.config['etc_dir'], self.app.config['software_profile']) def setupProjectFolder(self): """Helper to create a project folder as for slapos.git""" base = os.path.join(self.app.config['workspace'], 'slapos') software = os.path.join(base, 'software') os.mkdir(base) os.mkdir(software) def setupTestSoftware(self): """Helper to setup Basic SR for testing purposes""" self.setupProjectFolder() base = os.path.join(self.app.config['workspace'], 'slapos') software = os.path.join(base, 'software') testSoftware = os.path.join(software, 'slaprunner-test') sr = "[buildout]\n\n" sr += "parts = command\n\nunzip = true\nnetworkcache-section = networkcache\n\n" sr += "find-links += http://www.nexedi.org/static/packages/source/slapos.buildout/\n\n" sr += "[networkcache]\ndownload-cache-url = http://www.shacache.org/shacache" sr += "\ndownload-dir-url = http://www.shacache.org/shadir\n\n" sr += "[command]\nrecipe = zc.recipe.egg\neggs = plone.recipe.command\n zc.buildout\n\n" sr += """ [versions] setuptools = 33.1.1 """ os.mkdir(testSoftware) open(os.path.join(testSoftware, self.app.config['software_profile']), 'w').write(sr) md5 = hashlib.md5(os.path.join(self.app.config['workspace'], "slapos/software/slaprunner-test", self.app.config['software_profile']) ).hexdigest() base = os.path.join(self.app.config['software_root'], md5) template = os.path.join(base, self.template) content = "[buildout]\n" content += "parts = \n create-file\n\n" content += "eggs-directory = %s\n" % os.path.join(base, 'eggs') content += "develop-eggs-directory = %s\n\n" % os.path.join(base, 'develop-eggs') content += "[create-file]\nrecipe = plone.recipe.command\n" content += "filename = ${buildout:directory}/etc\n" content += "command = mkdir ${:filename} && echo 'simple file' > ${:filename}/testfile\n" os.mkdir(self.app.config['software_root']) os.mkdir(base) open(template, "w").write(content) def assertCanLoginWith(self, username, password): request = Request(self.backend_url) base64string = base64.encodestring('%s:%s' % (username, password))[:-1] request.add_header("Authorization", "Basic %s" % base64string) ssl_context = ssl._create_unverified_context() result = urlopen(request, context=ssl_context) self.assertEqual(result.getcode(), 200) def test_updateAccount(self): """test Update accound, this needs the user to log in""" # Check that given user can log in self.assertCanLoginWith(self.users[0], self.users[1]) # Adds a user new_user = {'username': 'vice-president', 'password': '123456'} response = loadJson(self.app.post('/addUser', data=new_user)) self.assertEqual(response['code'], 1) self.assertCanLoginWith(new_user['username'], new_user['password']) def test_cloneProject(self): """Start scenario 1 for deploying SR: Clone a project from git repository""" folder = 'workspace/' + self.project if os.path.exists(self.app.config['workspace'] + '/' + self.project): shutil.rmtree(self.app.config['workspace'] + '/' + self.project) data = { 'repo': self.repo, 'user': 'Slaprunner test', 'email': 'slaprunner@nexedi.com', 'name': folder } response = loadJson(self.app.post('/cloneRepository', data=data, follow_redirects=True)) self.assertEqual(response['result'], "") # Get realpath of create project path_data = dict(file=folder) response = loadJson(self.app.post('/getPath', data=path_data, follow_redirects=True)) self.assertEqual(response['code'], 1) realFolder = response['result'].split('#')[0] #Check git configuration config = open(os.path.join(realFolder, '.git/config')).read() assert "slaprunner@nexedi.com" in config and "Slaprunner test" in config # Checkout to slaprunner branch, this supposes that branch slaprunner exit response = loadJson(self.app.post('/newBranch', data=dict( project=folder, create='0', name='erp5' ), follow_redirects=True)) self.assertEqual(response['result'], "") def test_createSR(self): """Scenario 2: Create a new software release""" #setup project directory self.setupProjectFolder() newSoftware = os.path.join(self.software, 'slaprunner-test') response = loadJson(self.app.post('/createSoftware', data=dict(folder=newSoftware), follow_redirects=True)) self.assertEqual(response['result'], "") currentSR = self.getCurrentSR() assert newSoftware in currentSR def test_openSR(self): """Scenario 3: Open software release""" self.test_cloneProject() software = os.path.join(self.software, 'helloworld') # Drupal SR must exist in SR folder response = loadJson(self.app.post('/setCurrentProject', data=dict(path=software), follow_redirects=True)) self.assertEqual(response['result'], "") currentSR = self.getCurrentSR() assert software in currentSR self.assertFalse(isInstanceRunning(self.app.config)) self.assertFalse(isSoftwareRunning(self.app.config)) def test_runSoftware(self): """Scenario 4: CReate empty SR and save software.cfg file then run slapgrid-sr """ #Call config account #call create software Release self.test_createSR() newSoftware = self.getCurrentSR() softwareRelease = "[buildout]\n\nparts =\n test-application\n" softwareRelease += "#Test download git web repos éè@: utf-8 caracters\n" softwareRelease += "[test-application]\nrecipe = hexagonit.recipe.download\n" softwareRelease += "url = https://lab.nexedi.com/rafael/slapos-workarround.git\n" softwareRelease += "filename = slapos.git\n" softwareRelease += "download-only = true\n" softwareRelease += "[versions]\nsetuptools = 33.1.1" response = loadJson(self.app.post('/saveFileContent', data=dict(file=newSoftware, content=softwareRelease), follow_redirects=True)) self.assertEqual(response['result'], "") # Compile software and wait until slapgrid ends # this is supposed to use current SR while self.app.get('/isSRReady').data == "2": time.sleep(2) self.assertEqual(self.app.get('/isSRReady').data, "1") self.assertTrue(os.path.exists(self.app.config['software_root'])) self.assertTrue(os.path.exists(self.app.config['software_log'])) assert "test-application" in open(self.app.config['software_log']).read() sr_dir = os.listdir(self.app.config['software_root']) self.assertEqual(len(sr_dir), 1) createdFile = os.path.join(self.app.config['software_root'], sr_dir[0], 'parts', 'test-application', 'slapos.git') self.assertTrue(os.path.exists(createdFile)) def test_updateInstanceParameter(self): """Scenario 5: Update parameters of current sofware profile""" self.setupTestSoftware() #Set current projet and run Slapgrid-cp software = self.software + 'slaprunner-test/' response = loadJson(self.app.post('/setCurrentProject', data=dict(path=software), follow_redirects=True)) self.assertEqual(response['result'], "") #Send paramters for the instance parameterDict = dict(appname='slaprunnerTest', cacountry='France') parameterXml = '\n' parameterXml += 'slaprunnerTest\n' parameterXml += 'France\n' software_type = 'production' response = loadJson(self.app.post('/saveParameterXml', data=dict(parameter=parameterXml, software_type=software_type), follow_redirects=True)) self.assertEqual(response['result'], "") slap = slapos.slap.slap() slap.initializeConnection(self.app.config['master_url']) computer = slap.registerComputer(self.app.config['computer_id']) partitionList = computer.getComputerPartitionList() self.assertNotEqual(partitionList, []) #Assume that the requested partition is partition 0 slapParameterDict = partitionList[0].getInstanceParameterDict() self.assertTrue('appname' in slapParameterDict) self.assertTrue('cacountry' in slapParameterDict) self.assertEqual(slapParameterDict['appname'], 'slaprunnerTest') self.assertEqual(slapParameterDict['cacountry'], 'France') self.assertEqual(slapParameterDict['slap_software_type'], 'production') #test getParameterXml for webrunner UI response = loadJson(self.app.get('/getParameterXml/xml')) self.assertEqual(parameterXml, response['result']) response = loadJson(self.app.get('/getParameterXml/dict')) self.assertEqual(parameterDict, response['result']['instance']) def test_requestInstance(self): """Scenario 6: request software instance""" self.test_updateInstanceParameter() #run Software profile response = loadJson(self.app.post('/runSoftwareProfile', data=dict(), follow_redirects=True)) while self.app.get('/isSRReady').data == "2": time.sleep(2) self.assertEqual(self.app.get('/isSRReady').data, "1") #run instance profile response = loadJson(self.app.post('/runInstanceProfile', data=dict(), follow_redirects=True)) self.assertTrue(response['result']) # lets some time to the Instance to be deployed time.sleep(5) #Check that all partitions has been created assert "create-file" in open(self.app.config['instance_log']).read() for num in range(int(self.app.config['partition_amount'])): partition = os.path.join(self.app.config['instance_root'], self.partitionPrefix + str(num)) self.assertTrue(os.path.exists(partition)) #Go to partition 0 instancePath = os.path.join(self.app.config['instance_root'], self.partitionPrefix + '0') createdFile = os.path.join(instancePath, 'etc', 'testfile') self.assertTrue(os.path.exists(createdFile)) assert 'simple file' in open(createdFile).read() def test_safeAutoDeploy(self): """Scenario 7: isSRReady won't overwrite the existing Sofware Instance if it has been deployed yet""" # Test that SR won't be deployed with auto_deploy=False self.updateConfigParameter('auto_deploy', False) self.updateConfigParameter('autorun', False) project = open(os.path.join(self.app.config['etc_dir'], '.project'), "w") project.write(self.software + 'slaprunner-test/') project.close() response = isSoftwareReleaseReady(self.app.config) self.assertEqual(response, "0") # Test if auto_deploy parameter starts the deployment of SR self.updateConfigParameter('auto_deploy', True) self.setupTestSoftware() response = isSoftwareReleaseReady(self.app.config) self.assertEqual(response, "2") # Test that the new call to isSoftwareReleaseReady # doesn't overwrite the previous installed one sup_process.killRunningProcess(self.app.config, 'slapgrid-sr') completed_path = os.path.join(self.app.config['runner_workdir'], 'softwareLink', 'slaprunner-test', '.completed') completed_text = ".completed file: test" completed = open(completed_path, "w") completed.write(completed_text) completed.close() response = isSoftwareReleaseReady(self.app.config) self.assertEqual(response, "1") assert completed_text in open(completed_path).read() def test_maximumRunOfSlapgrid(self): """Scenario 8: runSlapgridUntilSucces run until a defined maximum of time slapgrid-sr and slapgrid-cp if it fails. It can also run only one or both of them if it is defined so We directly calls runSlapgridUntilSuccess, because we want to test the return code of the function""" # Installs a wrong buildout which will fail MAX_RUN_SOFTWARE = getBuildAndRunParams(self.app.config)['max_run_software'] MAX_RUN_INSTANCE = getBuildAndRunParams(self.app.config)['max_run_instance'] self.test_createSR() newSoftware = self.getCurrentSR() softwareRelease = "[buildout]\n\nparts =\n test-application\n" softwareRelease += "find-links += http://www.nexedi.org/static/packages/source/slapos.buildout/\n\n" softwareRelease += "[networkcache]\ndownload-cache-url = http://www.shacache.org/shacache" softwareRelease += "\ndownload-dir-url = http://www.shacache.org/shadir\n\n" softwareRelease += "#Test download git web repos éè@: utf-8 caracters\n" softwareRelease += "[test-application]\nrecipe = slapos.cookbook:mkdirectory\n" softwareRelease += "test = /root/test\n" softwareRelease += """ [versions] setuptools = 33.1.1 """ response = loadJson(self.app.post('/saveFileContent', data=dict(file=newSoftware, content=softwareRelease), follow_redirects=True)) response = runSlapgridUntilSuccess(self.app.config, 'software') self.assertEqual(response, MAX_RUN_SOFTWARE) # clean folders for other tests workdir = os.path.join(self.app.config['runner_workdir'], 'project') git_repo = os.path.join(workdir, 'slapos') if os.path.exists(git_repo): shutil.rmtree(git_repo) # Installs a software which deploys, but fails while instanciating # preparation base = os.path.join(self.app.config['workspace'], 'slapos') software = os.path.join(base, 'software') testSoftware = os.path.join(software, 'slaprunner-test') if not os.path.exists(testSoftware): os.makedirs(testSoftware) software_cfg = os.path.join(testSoftware, 'software.cfg') instance_cfg = os.path.join(testSoftware, 'instance.cfg') # software.cfg softwareRelease = "[buildout]\n\nparts =\n failing-template\n\n" softwareRelease += "[failing-template]\nrecipe = hexagonit.recipe.download\n" softwareRelease += "url = %s\n" % (instance_cfg) softwareRelease += "destination = ${buildout:directory}\n" softwareRelease += "download-only = true\n" open(software_cfg, 'w+').write(softwareRelease) # instance.cfg content = "[buildout]\n\nparts =\n fail\n" content += "[fail]\nrecipe=plone.recipe.command\n" content += "command = exit 1" open(instance_cfg, 'w+').write(content) project = open(os.path.join(self.app.config['etc_dir'], '.project'), "w") project.write(self.software + 'slaprunner-test') project.close() # Build and Run parameters = getBuildAndRunParams(self.app.config) parameters['run_instance'] = False saveBuildAndRunParams(self.app.config, parameters) response = runSlapgridUntilSuccess(self.app.config, 'software') self.assertEqual(response, 1) parameters['run_instance'] = True saveBuildAndRunParams(self.app.config, parameters) response = runSlapgridUntilSuccess(self.app.config, 'software') self.assertEqual(response, (1, MAX_RUN_INSTANCE)) def test_slaveInstanceDeployment(self): """ In order to test both slapproxy and core features of slaprunner, will install special Software Release into the current webrunner and fetch its instance parameters once deployed. """ # XXX: This test should NOT be a unit test but should be run # by a Test Agent running against a slapproxy. # Deploy "test-slave-instance-deployment" Software Release self.test_cloneProject() software = os.path.join(self.software, 'test-slave-instance-deployment') # Checkout to master branch response = loadJson(self.app.post('/newBranch', data=dict( project=self.workdir+'/slapos', create='0', name='master' ), follow_redirects=True)).get(u'code') self.assertEqual(response, 1) response = loadJson(self.app.post('/setCurrentProject', data=dict(path=software), follow_redirects=True)).get(u'code') self.assertEqual(response, 1) response = loadJson(self.app.post('/saveParameterXml', data=dict(parameter='\n', software_type='default'), follow_redirects=True)) while self.app.get('/isSRReady').data == "2": time.sleep(2) self.assertEqual(self.app.get('/isSRReady').data, "1") # Run instance deployment 3 times runInstanceWithLock(self.app.config) runInstanceWithLock(self.app.config) result = runInstanceWithLock(self.app.config) # result is True if returncode is 0 (i.e "deployed and promise passed") self.assertTrue(result) def test_dynamicParametersReading(self): """Test if the value of a parameter can change in the flask application only by changing the value of slapos.cfg config file. This can happen when slapgrid processes the webrunner's partition. """ config_file = os.path.join(self.app.config['etc_dir'], 'slapos-test.cfg') runner_config_old = os.environ['RUNNER_CONFIG'] os.environ['RUNNER_CONFIG'] = config_file open(config_file, 'w').write("[section]\nvar=value") config = self.app.config self.assertEqual(config['var'], "value") open(config_file, 'w').write("[section]\nvar=value_changed") self.assertEqual(config['var'], "value_changed") # cleanup os.environ['RUNNER_CONFIG'] = runner_config_old class PrintStringIO(StringIO): def write(self, data): StringIO.write(self, data) print(data) def main(): """ Function meant to be run by erp5testnode. """ args = parseArguments() master = taskdistribution.TaskDistributor(args.master_url) test_suite_title = args.test_suite_title or args.test_suite revision = args.revision SlaprunnerTestCase.setUpClass( key_file=args.key_file, cert_file=args.cert_file, server_url=args.server_url, computer_id=args.computer_id, partition_id=args.partition_id) test_result = master.createTestResult(revision, [test_suite_title], args.test_node_title, True, test_suite_title, args.project_title) if test_result is None: # Thereis nothing to run here, all tests are been running by # some other node. return test_line = test_result.start() start_time = time.time() stderr = PrintStringIO() suite = unittest.TestLoader().loadTestsFromTestCase(SlaprunnerTestCase) test_result = unittest.TextTestRunner(verbosity=2, stream=stderr).run(suite) test_duration = time.time() - start_time test_line.stop(stderr=stderr.getvalue(), test_count=test_result.testsRun, error_count=len(test_result.errors), failure_count=len(test_result.failures) + len(test_result.unexpectedSuccesses), skip_count=len(test_result.skipped), duration=test_duration) def runStandaloneUnitTest(): """ Run unit tests without erp5testnode." """ unittest.main(module=__name__)