Commit 528ca993 authored by Benjamin Blanc's avatar Benjamin Blanc Committed by Sebastien Robin

testnode: Modify testnode structure to include scalability tests.

Testnode code has been splitted. Runner class has been created for
each test types (unit and scalability).
Communication with SlapOS and ERP5 Master has been added.
parent a7310bc8
This diff is collapsed.
......@@ -138,6 +138,15 @@ class TestResultLineProxy(RPCRetry):
def name(self):
return self._name
def isTestCaseAlive(self):
"""
Tell if test result line is still alive on site.
"""
try:
return bool(self._retryRPC('isTestCaseAlive', [self._test_result_line_path]))
except:
raise ValueError('isTestCaseAlive Failed.')
def stop(self, test_count=None, error_count=None, failure_count=None,
skip_count=None, duration=None, date=None, command=None,
stdout=None, stderr=None, html_test_result=None, **kw):
......@@ -201,6 +210,10 @@ class TestResultProxy(RPCRetry):
return '<%s(%r, %r, %r) at %x>' % (self.__class__.__name__,
self._test_result_path, self._node_title, self._revision, id(self))
@property
def test_result_path(self):
return self._test_result_path
@property
def revision(self):
return self._revision
......@@ -350,6 +363,35 @@ class TestResultProxy(RPCRetry):
if self._watcher_thread is not None:
self._watcher_thread.join()
def stop(self):
"""
"""
return self._retryRPC('stopTest', [self._test_result_path])
class TestResultProxyProxy(TestResultProxy):
"""
A wrapper/proxy to TestResultProxy
"""
def __init__(self, test_suite_master_url, retry_time, logger, test_result_path,
node_title, revision):
try:
proxy = ServerProxy(
test_suite_master_url,
allow_none=True,
).portal_task_distribution
except:
raise ValueError("Cannot instanciate ServerProxy")
TestResultProxy.__init__(self, proxy, retry_time, logger, test_result_path,
node_title, revision)
def getRunningTestCase(self):
"""
A proxy to getNextTestCase
Return the relative path of the test with the running state
"""
return self._retryRPC('getRunningTestCase', [self._test_result_path])
class ServerProxy(xmlrpclib.ServerProxy):
def __init__(self, *args, **kw):
......@@ -427,7 +469,6 @@ class TaskDistributionTool(RPCRetry):
class TaskDistributor(RPCRetry):
def __init__(self,portal_url,retry_time=64,logger=None):
if logger is None:
logger = null_logger
if portal_url is None:
......@@ -440,14 +481,67 @@ class TaskDistributor(RPCRetry):
raise ValueError('Unsupported protocol revision: %r',
protocol_revision)
def startTestSuite(self,node_title):
def startTestSuite(self,node_title,computer_guid='unknown'):
"""
Returns None if no test suite is needed.
therwise, returns a JSON with all the test suite parameters.
"""
result = self._retryRPC('startTestSuite',(node_title,))
result = self._retryRPC('startTestSuite',(node_title,computer_guid,))
return result
def getTestType(self):
"""
Return the Test Type
"""
result = self._retryRPC('getTestType')
return result
def subscribeNode(self, node_title, computer_guid):
"""
Susbscribes node with the node title and the computer guid.
"""
self._retryRPC('subscribeNode', (node_title,computer_guid,))
def generateConfiguration(self, test_suite_title):
"""
Generates a configuration from a test_suite_title
"""
return self._retryRPC('generateConfiguration', (test_suite_title,))
def isMasterTestnode(self, test_node_title):
"""
Returns True or False if the testnode is the master
"""
return self._retryRPC('isMasterTestnode', (test_node_title,))
def getSlaposAccountKey(self):
"""
Returns the slapos account key related to the distributor
"""
return self._retryRPC('getSlaposAccountKey')
def getSlaposAccountCertificate(self):
"""
Returns the slapos account certificate related to the distributor
"""
return self._retryRPC('getSlaposAccountCertificate')
def getSlaposUrl(self):
"""
Returns the url of slapos master related to the distributor
"""
return self._retryRPC('getSlaposUrl')
def getSlaposHateoasUrl(self):
"""
Returns the url of API REST using hateoas of
slapos master related to the distributor
"""
return self._retryRPC('getSlaposHateoasUrl')
class DummyTaskDistributionTool(object):
"""
Fake remote server.
......
##############################################################################
#
# Copyright (c) 2011 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 advised 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.
#
##############################################################################
from datetime import datetime,timedelta
import os
import subprocess
import sys
import time
import glob
import SlapOSControler
import json
import time
import shutil
import logging
import string
import random
from ProcessManager import SubprocessError, ProcessManager, CancellationError
from subprocess import CalledProcessError
from Updater import Updater
from erp5.util import taskdistribution
class SlapOSInstance(object):
"""
Base of an software instance,
store variables used during software installation
"""
def __init__(self):
self.retry_software_count = 0
self.retry = False
def edit(self, **kw):
self.__dict__.update(**kw)
self._checkData()
def _checkData(self):
pass
class NodeTestSuite(SlapOSInstance):
"""
"""
def __init__(self, reference):
super(NodeTestSuite, self).__init__()
self.reference = reference
def edit(self, **kw):
super(NodeTestSuite, self).edit(**kw)
def _checkData(self):
if getattr(self, "working_directory", None) is not None:
if not(self.working_directory.endswith(os.path.sep + self.reference)):
self.working_directory = os.path.join(self.working_directory,
self.reference)
SlapOSControler.createFolder(self.working_directory)
self.test_suite_directory = os.path.join(
self.working_directory, "test_suite")
self.custom_profile_path = os.path.join(self.working_directory,
'software.cfg')
if getattr(self, "vcs_repository_list", None) is not None:
for vcs_repository in self.vcs_repository_list:
buildout_section_id = vcs_repository.get('buildout_section_id', None)
repository_id = buildout_section_id or \
vcs_repository.get('url').split('/')[-1].split('.')[0]
repository_path = os.path.join(self.working_directory,repository_id)
vcs_repository['repository_id'] = repository_id
vcs_repository['repository_path'] = repository_path
def createSuiteLog(self):
# /srv/slapgrid/slappartXX/srv/var/log/testnode/az-mlksjfmlk234Sljssdflkj23KSdfslj/suite.log
alphabets = string.digits + string.letters
rand_part = ''.join(random.choice(alphabets) for i in xrange(32))
random_suite_folder_id = '%s-%s' % (self.reference, rand_part)
suite_log_directory = os.path.join(self.log_directory,
random_suite_folder_id)
SlapOSControler.createFolders(suite_log_directory)
self.suite_log_path = os.path.join(suite_log_directory,
'suite.log')
return self.getSuiteLogPath(), random_suite_folder_id
def getSuiteLogPath(self):
return getattr(self,"suite_log_path", None)
This diff is collapsed.
......@@ -33,6 +33,8 @@ import xml_marshaller
import shutil
import sys
import glob
import argparse
from slapos import client
MAX_PARTIONS = 10
MAX_SR_RETRIES = 3
......@@ -47,6 +49,20 @@ def createFolders(folder):
if not(os.path.exists(folder)):
os.makedirs(folder)
def isDir(folder):
return os.path.isdir(folder)
def createFile(path, mode, content):
f = open(path, mode)
if os.path.exists(path):
f.write(content)
f.close()
else:
# error
pass
class SlapOSControler(object):
def __init__(self, working_directory, config, log):
......@@ -54,8 +70,175 @@ class SlapOSControler(object):
self.software_root = os.path.join(working_directory, 'soft')
self.instance_root = os.path.join(working_directory, 'inst')
self.slapos_config = os.path.join(working_directory, 'slapos.cfg')
self.proxy_database = os.path.join(working_directory, 'proxy.db')
self.log = log
self.proxy_database = os.path.join(working_directory, 'proxy.db')
self.instance_config = {}
#TODO: implement a method to get all instance related the slapOS account
# and deleting all old instances (based on creation date or name etc...)
def createSlaposConfigurationFileAccount(self, key, certificate, slapos_url, config):
# Create "slapos_account" directory in the "slapos_directory"
slapos_account_directory = os.path.join(config['slapos_directory'], "slapos_account")
createFolder(slapos_account_directory)
# Create slapos-account files
slapos_account_key_path = os.path.join(slapos_account_directory, "key")
slapos_account_certificate_path = os.path.join(slapos_account_directory, "certificate")
configuration_file_path = os.path.join(slapos_account_directory, "slapos.cfg")
configuration_file_value = "[slapos]\nmaster_url = %s\n\
[slapconsole]\ncert_file = %s\nkey_file = %s" %(
slapos_url,
slapos_account_certificate_path,
slapos_account_key_path)
createFile(slapos_account_key_path, "w", key)
createFile(slapos_account_certificate_path, "w", certificate)
createFile(configuration_file_path, "w", configuration_file_value)
self.configuration_file_path = configuration_file_path
return slapos_account_key_path, slapos_account_certificate_path, configuration_file_path
def supply(self, software_url, computer_id, state="available"):
"""
Request the installation of a software release on a specific node
Ex :
my_controler.supply('kvm.cfg', 'COMP-726')
"""
self.log('SlapOSControler : supply')
parser = argparse.ArgumentParser()
parser.add_argument("configuration_file")
parser.add_argument("software_url")
parser.add_argument("node")
if os.path.exists(self.configuration_file_path):
args = parser.parse_args([self.configuration_file_path, software_url, computer_id])
config = client.Config()
config.setConfig(args, args.configuration_file)
try:
local = client.init(config)
local['supply'](software_url, computer_guid=computer_id, state=state)
self.log('SlapOSControler : supply %s %s %s' %(software_url, computer_id, state))
except:
self.log("SlapOSControler.supply, \
exception in registerOpenOrder", exc_info=sys.exc_info())
raise ValueError("Unable to supply (or remove)")
else:
raise ValueError("Configuration file not found.")
def destroy(self, software_url, computer_id):
"""
Request Deletetion of a software release on a specific node
Ex :
my_controler.destroy('kvm.cfg', 'COMP-726')
"""
self.supply(self, software_url, computer_id, state="destroyed")
def getInstanceRequestedState(self, reference):
try:
return self.instance_config[reference]['requested_state']
except:
raise ValueError("Instance '%s' not exist" %self.instance_config[reference])
def request(self, reference, software_url, software_type=None,
software_configuration=None, computer_guid=None, state='started'):
"""
configuration_file_path (slapos acount)
reference : instance title
software_url : software path/url
software_type : scalability
software_configuration : dict { "_" : "{'toto' : 'titi'}" }
Ex :
my_controler._request('Instance16h34Ben',
'kvm.cfg', 'cluster', { "_" : "{'toto' : 'titi'}" } )
"""
self.log('SlapOSControler : request-->SlapOSMaster')
current_intance_config = {'software_type':software_type,
'software_configuration':software_configuration,
'computer_guid':computer_guid,
'software_url':software_url,
'requested_state':state,
'partition':None
}
self.instance_config[reference] = current_intance_config
filter_kw = None
if computer_guid != None:
filter_kw = { "computer_guid": computer_guid }
if os.path.exists(self.configuration_file_path):
parser = argparse.ArgumentParser()
parser.add_argument("configuration_file")
args = parser.parse_args([self.configuration_file_path])
config = client.Config()
config.setConfig(args, args.configuration_file)
try:
local = client.init(config)
partition = local['request'](
software_release = software_url,
partition_reference = reference,
partition_parameter_kw = software_configuration,
software_type = software_type,
filter_kw = filter_kw,
state = state)
self.instance_config[reference]['partition'] = partition
if state == 'destroyed':
del self.instance_config[reference]
if state == 'started':
self.log('Instance started with configuration: %s' %str(software_configuration))
except:
self.log("SlapOSControler.request, \
exception in registerOpenOrder", exc_info=sys.exc_info())
raise ValueError("Unable to do this request")
else:
raise ValueError("Configuration file not found.")
def _requestSpecificState(self, reference, state):
self.request(reference,
self.instance_config[reference]['software_url'],
self.instance_config[reference]['software_type'],
self.instance_config[reference]['software_configuration'],
self.instance_config[reference]['computer_guid'],
state=state
)
def destroyInstance(self, reference):
self.log('SlapOSControler : delete instance')
try:
self._requestSpecificState(reference, 'destroyed')
except:
raise ValueError("Can't delete instance '%s' (instance may not been created?)" %reference)
def stopInstance(self, reference):
self.log('SlapOSControler : stop instance')
try:
self._requestSpecificState(reference, 'stopped')
except:
raise ValueError("Can't stop instance '%s' (instance may not been created?)" %reference)
def startInstance(self, reference):
self.log('SlapOSControler : start instance')
try:
self._requestSpecificState(reference, 'started')
except:
raise ValueError("Can't start instance '%s' (instance may not been created?)" %reference)
def updateInstanceXML(self, reference, software_configuration):
"""
Update the XML configuration of an instance
# Request same instance with different parameters.
"""
self.log('SlapOSControler : updateInstanceXML')
self.log('SlapOSControler : updateInstanceXML will request same'
'instance with new XML configuration...')
try:
self.request(reference,
self.instance_config[reference]['software_url'],
self.instance_config[reference]['software_type'],
software_configuration,
self.instance_config[reference]['computer_guid'],
state='started'
)
except:
raise ValueError("Can't update instance '%s' (may not exist?)" %reference)
def _resetSoftware(self):
self.log('SlapOSControler : GOING TO RESET ALL SOFTWARE : %r' %
......@@ -65,7 +248,6 @@ class SlapOSControler(object):
os.mkdir(self.software_root)
os.chmod(self.software_root, 0750)
def initializeSlapOSControler(self, slapproxy_log=None, process_manager=None,
reset_software=False, software_path_list=None):
self.process_manager = process_manager
......
import json
import httplib
import urlparse
import time
TIMEOUT = 30
# TODO: News-> look list to get last news... (and not the first of the list)
class SlapOSMasterCommunicator(object):
"""
Communication with slapos Master using Hateoas.
collection: collection of data (hosting_subscription, instance, software_release...)
hosting_subscription: result of a request
instance(s): instance(s) related to an hosting_subscription
usage: ex:
# Try to reuse same communicator, because initilization step may takes a lot of time
# due to listing of all instances (alive or not) related to the specified slapOS account.
communicator = SlapOSMasterCommunicator()
# Print news related to 'TestScalability_21423104630420' all instances
instance_link_list = communicator._getRelatedInstanceLink('TestScalability_21423104630420')
for instance_link in instance_link_list:
news = communicator.getNewsFromInstanceLink(instance_link)
print news['news']
"""
def __init__(self, certificate_path, key_path, log,
url):
# Create connection
api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(url)
self.log = log
self.certificate_path = certificate_path
self.key_path = key_path
self.url = url
self.connection = self._getConnection(self.certificate_path, self.key_path, self.url)
# Get master
master_link = {'href':api_path,'type':"application/vnd.slapos.org.hal+json; class=slapos.org.master"}
master = self._curl(master_link)
self.person_link = master['_links']['http://slapos.org/reg/me']
# Get person related to specified key/certificate provided
person = self._curl(self.person_link)
self.personnal_collection_link = person['_links']['http://slapos.org/reg/hosting_subscription']
# Get collection (of hosting subscriptions)
collection = self._curl(self.personnal_collection_link)
# XXX: This part may be extremly long (because here no hosting subscriptions
# has been visited)
self.hosting_subcriptions_dict = {}
self.visited_hosting_subcriptions_link_list = []
self.log("SlapOSMasterCommunicator will read all hosting subscriptions entries, "
"it may take several time...")
self._update_hosting_subscription_informations()
def _getConnection(self,certificate_path, key_path, url):
api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(url)
#self.log("HTTPS Connection with: %s, cert=%s, key=%s" %(api_netloc,key_path,certificate_path))
return httplib.HTTPSConnection(api_netloc, key_file=key_path, cert_file=certificate_path, timeout=TIMEOUT)
def _curl(self, link):
"""
'link' must look like : {'href':url,'type':content_type}
"""
# Set timeout
import socket
socket.setdefaulttimeout(1.0*TIMEOUT)
api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(link['href'])
max_retry = 10
# Try to use existing conection
try:
self.connection.request(method='GET', url=api_path, headers={'Accept': link['type']}, body="")
response = self.connection.getresponse()
return json.loads(response.read())
# Create and use new connection
except:
retry = 0
# (re)Try several time to use new connection
while retry < max_retry:
try:
self.connection = self._getConnection(self.certificate_path, self.key_path, self.url)
self.connection.request(method='GET', url=api_path, headers={'Accept': link['type']}, body="")
response = self.connection.getresponse()
return json.loads(response.read())
except:
self.log("SlapOSMasterCommunicator: Connection failed..")
retry += 1
time.sleep(10)
self.log("SlapOSMasterCommunicator: All connection attempts failed after %d try.." %max_retry)
raise ValueError("SlapOSMasterCommunicator: Impossible to use connection")
def _update_hosting_subscription_informations(self):
"""
Add all not already visited hosting_subcription
# Visit all hosting subscriptions and fill a dict containing all
# new hosting subscriptions. ( like: {hs1_title:hs1_link, hs2_title:hs2_link, ..} )
# and a list of visited hosting_subsciption ( like: [hs1_link, hs2_link, ..] )
"""
collection = self._curl(self.personnal_collection_link)
# For each hosting_subcription present in the collection
for hosting_subscription_link in collection['_links']['item']:
if hosting_subscription_link not in self.visited_hosting_subcriptions_link_list:
hosting_subscription = self._curl(hosting_subscription_link)
self.hosting_subcriptions_dict.update({hosting_subscription['title']:hosting_subscription_link})
self.visited_hosting_subcriptions_link_list.append(hosting_subscription_link)
def _getRelatedInstanceLink(self, hosting_subscription_title):
"""
Return a list of all related instance_url from an hosting_subscription_title
"""
# Update informations
self._update_hosting_subscription_informations()
# Get specified hosting_subscription
hosting_subscription_link = self.hosting_subcriptions_dict[hosting_subscription_title]
hosting_subscription = self._curl(hosting_subscription_link)
assert(hosting_subscription_title == hosting_subscription['title'])
# Get instance collection related to this hosting_subscription
instance_collection_link = hosting_subscription['_links']['http://slapos.org/reg/instance']
instance_collection = self._curl(instance_collection_link)
related_instance_link_list = []
# For each instance present in the collection
for instance in instance_collection['_links']['item']:
related_instance_link_list.append(instance)
return related_instance_link_list
def getNewsFromInstanceLink(self, instance_link):
instance = self._curl(instance_link)
news_link = instance['_links']['http://slapos.org/reg/news']
return self._curl(news_link)
def isHostingSubsciptionStatusEqualTo(self, hosting_subscription_title, excepted_news_text):
"""
Return True if all related instance state are equal to status,
or False if not or if there is are no related instances.
"""
related_instance_link_list = _getRelatedInstanceLink(hosting_subscription_title)
# For each instance present in the collection
for instance_link in related_instance_link_list:
news = self.getNewsFromInstanceLink(instance_link)
if excepted_news_text != news['news'][0]['text']:
return False
return len(related_instance_link_list) > 0
def isInstanceReady(self, instance_link, status):
"""
Return True if instance status and instance news text ~looks corresponding.
( use the matching of 'correctly' and 'Instance' and status )
"""
# XXX: SlapOS Master doesn't store any "news" about slave instances. Assume true.
if self._curl(instance_link)['slave']:
return True
text = self.getNewsFromInstanceLink(instance_link)['news'][0]['text']
return ('Instance' in text) and ('correctly' in text) and (status in text)
# check if provided 'status' = status
def isHostingSubscriptionReady(self, hosting_subscription_title, status):
"""
Return True if all instance status and instance news text ~looks corresponding.
( use the matching of 'correctly' and 'Instance' and status ).
"""
instance_link_list = self._getRelatedInstanceLink(hosting_subscription_title)
for instance_link in instance_link_list:
if not self.isInstanceReady(instance_link, status):
return False
return len(instance_link_list) > 0
def isRegisteredHostingSubscription(self, hosting_subscription_title):
"""
Return True if the specified hosting_subscription is present on SlapOSMaster
"""
self._update_hosting_subscription_informations()
if self.hosting_subcriptions_dict.get(hosting_subscription_title):
return True
return False
def getHostingSubscriptionDict(self):
"""
Return the dict of hosting subcription.
"""
return self.hosting_subcriptions_dict
def getHostingSubscriptionInformationDict(self, title):
"""
Return a dict with informations about Hosting subscription
"""
related_instance_link_list = self._getRelatedInstanceLink(title)
related_instance_link = None
# Get root instance
for link in related_instance_link_list:
instance = self._curl(link)
if title == instance['title']:
related_instance_link = link
break
# Return information dict
if related_instance_link:
related_instance = self._curl(related_instance_link)
return {
'title': related_instance['title'],
'status': related_instance['status'],
'software_url': related_instance['_links']['http://slapos.org/reg/release'],
'software_type': related_instance['software_type'],
'computer_guid': related_instance['sla']['computer_guid']
}
else:
return None
\ No newline at end of file
##############################################################################
#
# Copyright (c) 2011 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 advised 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.
#
##############################################################################
from datetime import datetime,timedelta
import os
import subprocess
import sys
import time
import glob
import SlapOSControler
import json
import time
import shutil
import logging
import string
import random
from ProcessManager import SubprocessError, ProcessManager, CancellationError
from subprocess import CalledProcessError
from NodeTestSuite import SlapOSInstance