From b60799832218460f4f6af4e837c1e2a4a6ba2722 Mon Sep 17 00:00:00 2001 From: Syama Date: Thu, 11 Jun 2020 04:39:58 -0400 Subject: [PATCH 01/14] Added function for opensync restart and copying the AP certs --- py-scripts/cicd_TipIntegration.py | 494 ++++++++++++++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 py-scripts/cicd_TipIntegration.py diff --git a/py-scripts/cicd_TipIntegration.py b/py-scripts/cicd_TipIntegration.py new file mode 100644 index 00000000..eb8261d9 --- /dev/null +++ b/py-scripts/cicd_TipIntegration.py @@ -0,0 +1,494 @@ + +import base64 +import urllib.request +from bs4 import BeautifulSoup +import ssl +import subprocess, os +from artifactory import ArtifactoryPath +import tarfile +import paramiko +from paramiko import SSHClient +from scp import SCPClient +import os +import pexpect +from pexpect import pxssh +import sys +import paramiko +from scp import SCPClient +import pprint +from pprint import pprint + +local_dir=os.getenv('LOG_DIR') +print("Local Directory where all files will be copied and logged", local_dir) +cicd_user=os.getenv('CICD_USER') +print("cicd_user = ", cicd_user) +cicd_pw=os.getenv('CICD_PW') +print("cicd pw =",cicd_pw) +ap_pw=os.getenv('AP_PW') +ap_user=os.getenv('AP_USER') +tr_user=os.getenv('TR_USER') +print("Testrail user id = ", tr_user) +tr_pw=os.getenv('TR_PW') +print ("Testrail password =", tr_pw) +aws_host='3.96.56.0' +aws_user='ubuntu' + + + + +if sys.version_info[0] != 3: + print("This script requires Python 3") + exit(1) +if 'py-json' not in sys.path: + sys.path.append('../py-json') + +from LANforge.LFUtils import * +# if you lack __init__.py in this directory you will not find sta_connect module# +import sta_connect +import testrail_api +from sta_connect import StaConnect +from testrail_api import APIClient + +client: APIClient = APIClient('https://telecominfraproject.testrail.com') +client.user = tr_user +client.password = tr_pw + + +print('Beginning file download with requests') + +class GetBuild: + def __init__(self): + self.user = cicd_user + self.password = cicd_pw + ssl._create_default_https_context = ssl._create_unverified_context + + def get_latest_image(self,url): + + auth = str( + base64.b64encode( + bytes('%s:%s' % (cicd_user,cicd_pw ), 'utf-8') + ), + 'ascii' + ).strip() + headers = {'Authorization': 'Basic ' + auth} + + ''' FIND THE LATEST FILE NAME''' + print(url) + req = urllib.request.Request(url, headers=headers) + response = urllib.request.urlopen(req) + html = response.read() + soup = BeautifulSoup(html, features="html.parser") + last_link = soup.find_all('a', href=True)[-1] + latest_file=last_link['href'] + + filepath = local_dir + os.chdir(filepath) + #file_url = url + latest_file + + ''' Download the binary file from Jfrog''' + path = ArtifactoryPath(url,auth=(cicd_user, cicd_pw)) + path.touch() + for file in path: + print('File =', file) + + path = ArtifactoryPath(file, auth=(cicd_user, cicd_pw)) + print("file to be downloaded :" ,latest_file) + print("File Path:",file) + with path.open() as des: + with open(latest_file, "wb") as out: + out.write(des.read()) + des.close() + print("Extract the tar.gz file and upgrade the AP ") + housing_tgz = tarfile.open(latest_file) + housing_tgz.extractall() + housing_tgz.close() + return "pass" + print("Extract the tar file, and copying the file to Linksys AP directory") + #with open("/Users/syamadevi/Desktop/syama/ea8300/ap_sysupgrade_output.log", "a") as output: + # subprocess.call("scp /Users/syamadevi/Desktop/syama/ea8300/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin root@192.100.1.1:/tmp/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin",shell=True, stdout=output, + # stderr=output) + + print('SSH to Linksys and upgrade the file') + + ''' + + ssh = SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname='192.100.1.1', + port='22', + username='root', + password='Dadun123$', + look_for_keys=False, + pkey='load_key_if_relevant') + + # SCPCLient takes a paramiko transport as its only argument + scp = SCPClient(ssh.get_transport()) + + scp.put('test.txt', 'testD.txt') + scp.close() + + + + # client = paramiko.SSHClient() + #client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + #client.connect('192.100.1.1', username='syama', password='Dadun123$') + + stdin, stdout, stderr = ssh.exec_command('sysupgrade /tmp/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin') + + for line in stdout: + print (line.strip('\n')) + client.close() + ''' + + def run_opensyncgw_in_docker(self): + #user_password = 'fepv6nj9guCPeEHC' + #my_env = os.environ.copy() + #my_env["userpass"] = user_password + #my_command = 'python --version' + #subprocess.Popen('echo', env=my_env) + with open(local_dir +"docker_jfrog_login.log", "a") as output: + subprocess.call("docker login --username" + cicd_user + "--password" + cicd_pw + " https://tip-tip-wlan-cloud-docker-repo.jfrog.io", shell=True, stdout=output, + stderr=output) + with open(local_dir +"opensyncgw_upgrade.log", "a") as output: + subprocess.call("docker pull tip-tip-wlan-cloud-docker-repo.jfrog.io/opensync-gateway-and-mqtt:0.0.1-SNAPSHOT", shell=True, stdout=output, + stderr=output) + with open(local_dir+"opensyncgw.log", "a") as output: + subprocess.call("docker run --rm -i -p 1883:1883 -p 6640:6640 -p 6643:6643 -p 4043:4043 \ + -v ~/mosquitto/data:/mosquitto/data \ + -v ~/mosquitto/log:/mosquitto/log \ + -v ~/wlan-pki-cert-scripts:/opt/tip-wlan/certs \ + -v ~/app/log:/app/logs \ + -v ~//app/config:/app/config \ + -e OVSDB_CONFIG_FILE='/app/config/config_2_ssids.json' \ + tip-tip-wlan-cloud-docker-repo.jfrog.io/opensync-gateway-and-mqtt:0.0.1-SNAPSHOT",shell=True, stdout=output, + stderr=output) + print("opensync Gateway is running") + return "pass" + + def run_opensyncgw_in_aws(self): + try: + s = pxssh.pxssh() + + os.chdir(local_dir) + print("AWS OPENSYNC GW UPGRADE VIA HELM") + print( + 'Helm upgrades the latest image in the GW if a new image is found from jfrog and the AWS gateway is not upto date ') + # makesure the client key file is in the fame directory to login to AWS VM + s.login(aws_host, aws_user, ssh_key='id_key.pem') + s.sendline('kubectl get pods') + + # run a command + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.sendline( + 'helm upgrade tip-wlan wlan-cloud-helm/tip-wlan/ -n default -f wlan-cloud-helm/tip-wlan/resources/environments/dev-amazon.yaml') + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.sendline('kubectl get pods') + + # run a command + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.logout() + return "pass" + + except pxssh.ExceptionPxssh as e: + print("ALERT !!!!!! pxssh failed on login.") + print(e) + + +class openwrt_ap: + + def ap_upgrade(src,user2,host2,tgt,pwd,opts='', timeout=60): + ''' Performs the scp command. Transfers file(s) from local host to remote host ''' + print("AP Model getting upgarded is :", apModel) + if apModel == "ecw5410": + ap_firmware = 'openwrt-ipq806x-generic-edgecore_ecw5410-squashfs-nand-sysupgrade.bin' + AP_IP = '10.10.10.207' + else: + if apModel == "ea8300": + ap_firmware = 'openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin' + AP_IP = '10.10.10.208' + host2 = AP_IP + src = src+ ap_firmware + print("src =", src) + print("AP IP ", AP_IP) + print("AP USER =", ap_user) + print("AP PASSWORD =", ap_pw) + cmd = f'''/bin/bash -c "scp {opts} {src} {user2}@{AP_IP}:{tgt}"''' + print("Executing the following cmd:",cmd,sep='\n') + + tmpFl = '/tmp/scp.log' + fp = open(tmpFl,'wb') + print(tmpFl) + childP = pexpect.spawn(cmd,timeout=timeout) + try: + childP.sendline(cmd) + childP.expect([f"{user2}@{host2}'s password:"]) + childP.sendline(pwd) + childP.logfile = fp + childP.expect(pexpect.EOF) + childP.close() + fp.close() + fp = open(tmpFl,'r') + stdout = fp.read() + fp.close() + + if childP.exitstatus != 0: + raise Exception(stdout) + except KeyboardInterrupt: + childP.close() + fp.close() + return + print(stdout) + + try: + s = pxssh.pxssh() + s.login(host2, user2, pwd) + #s.sendline('sysupgrade /tmp/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin&') + s.sendline('sysupgrade /tmp/openwrt-ipq806x-generic-edgecore_ecw5410-squashfs-nand-sysupgrade.bin&') + #s.logout() + #s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + sleep(100) + #s.login(host2, user2, pwd) + s.prompt() + #os.system(f"scp {local_dir}/cacert.pem root@10.10.10.207:/usr/plume/certs/ca.pem") + #os.system(f"scp {local_dir}/clientcert.pem root@10.10.10.207:/usr/plume/certs/client.pem") + #os.system(f"scp {local_dir}/clientkey_dec.pem root@10.10.10.207:/usr/plume/certs/client_dec.key") + #s.sendline('service opensync restart') + #s.prompt() # match the prompt + #print(s.before) # print everything before the prompt. + s.logout() + return "pass" + except pxssh.ExceptionPxssh as e: + print("ALERT !!!!!! pxssh failed on login.") + print(e) + def apCopyCert(src,user2,host2,tgt,pwd,opts='', timeout=60): + + print("Copying the AP Certs") + ''' + s = pxssh.pxssh() + print(src, users2,pwd) + s.login(host2, user2, pwd) + s.prompt() # match the prompt + print("Copying ca.pem") + os.system(f"scp {src}/cacert.pem root@10.10.10.207:/usr/plume/certs/ca.pem") + print("Copying the client.pem") + os.system(f"scp {src}/clientcert.pem root@10.10.10.207:/usr/plume/certs/client.pem") + print("Copying the client_dec.key") + os.system(f"scp {src}/clientkey_dec.pem root@10.10.10.207:/usr/plume/certs/client_dec.key") + s.sendline('service opensync restart') + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.logout() + ''' + cacert=src+"ca.pem" + clientcert = src+"client.pem" + clientkey=src+"client_dec.key" + tgt ="/usr/plume/certs" + ap_pw + + print("src =", src) + print("AP IP ", host2) + print("AP USER =", ap_user) + print("AP PASSWORD =", ap_pw) + #cmd = f'''/bin/bash -c "scp {opts} {src} {user2}@{AP_IP}:{tgt}"''' + #cmd = f'''/bin/bash -c "scp {opts} {cacert} {user2}@{AP_IP}:{tgt}"''' + #cmd = f'''/bin/bash -c "scp {opts} {clientcert} {user2}@{AP_IP}:{tgt}"''' + cmd = f'''/bin/bash -c "scp {opts} {cacert} {clientcert} {clientkey} {user2}@{host2}:{tgt}"''' + print("Executing the following cmd:", cmd, sep='\n') + tmpFl = '/tmp/cert.log' + fp = open(tmpFl, 'wb') + print(tmpFl) + childP = pexpect.spawn(cmd, timeout=timeout) + try: + childP.sendline(cmd) + childP.expect([f"{user2}@{host2}'s password:"]) + childP.sendline(ap_pw) + childP.logfile = fp + childP.expect(pexpect.EOF) + fp.close() + fp = open(tmpFl,'r') + stdout = fp.read() + fp.close() + + if childP.exitstatus != 0: + #raise Exception(stdout) + print("there is an excess status non 0") + except KeyboardInterrupt: + childP.close() + fp.close() + return + print(stdout) + def restartGw(src,user2,host2,tgt,pwd,opts='', timeout=60): + print("Restarting opensync GW") + s = pxssh.pxssh() + s.login(host2, user2, pwd) + # s.sendline('sysupgrade /tmp/openwrt-ipq40xx-generic-linksys_ea8300-squashfs-sysupgrade.bin&') + s.sendline('service opensync restart') + # s.logout() + # s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.prompt() + s.logout() + + +class RunTest: + def TestCase_938(self, rid): + '''SINGLE CLIENT CONNECTIVITY''' + staConnect = StaConnect("10.10.10.201", 8080, _debugOn=False) + staConnect.sta_mode = 0 + staConnect.upstream_resource = 1 + staConnect.upstream_port = "eth2" + staConnect.radio = "wiphy1" + staConnect.resource = 1 + staConnect.dut_ssid = "autoProvisionedSsid-5u" + #staConnect.dut_passwd = "4C0nnectUS!" + staConnect.dut_passwd = "12345678" + staConnect.dut_security = "wpa2" + staConnect.station_names = ["sta01010"] + staConnect.runtime_secs = 30 + staConnect.cleanup_on_exit = True + staConnect.run() + run_results = staConnect.get_result_list() + for result in run_results: + print("test result: " + result) + #result = 'pass' + print("Single Client Connectivity :",staConnect.passes) + if staConnect.passes() == True: + client.update_testrail(case_id=938, run_id=rid, status_id=1, msg='client Connectivity to 5GHZ Open SSID is Passed ') + else: + client.update_testrail(case_id=938, run_id=rid, status_id=5, msg='client connectivity to 5GHZ OPEN SSID is Failed') + + def TestCase_941(self, rid): + #MULTI CLIENT CONNECTIVITY + staConnect = StaConnect("10.10.10.201", 8080, _debugOn=False) + staConnect.sta_mode = 0 + staConnect.upstream_resource = 1 + staConnect.upstream_port = "eth2" + staConnect.radio = "wiphy1" + staConnect.resource = 1 + staConnect.dut_ssid = "autoProvisionedSsid-5u" + # staConnect.dut_passwd = "4C0nnectUS!" + staConnect.dut_passwd = "12345678" + staConnect.dut_security = "wpa2" + staConnect.station_names = ["sta0020", 'sta0021', 'sta0022', 'sta0023'] + staConnect.runtime_secs = 20 + staConnect.cleanup_on_exit = True + staConnect.run() + run_results = staConnect.get_result_list() + for result in run_results: + print("test result: " + result) + if staConnect.passes() == True: + client.update_testrail(case_id=941, run_id=rid, status_id=1, + msg='client Connectivity to 5GHZ Open SSID is Passed ') + else: + client.update_testrail(case_id=941, run_id=rid, status_id=5, + msg='client connectivity to 5GHZ OPEN SSID is Failed') + + def TestCase_939(self, rid): + ''' Client Count in MQTT Log''' + try: + print("Counting clients in MQTT") + s = pxssh.pxssh() + #aws_host = os.getenv(AWS_HOST) + #aws_user=os.getenv(AWS_USER) + os.chdir(local_dir) + # makesure the client key file is in the fame directory to login to AWS VM + s.login(aws_host,aws_user,ssh_key='id_key.pem') + s.sendline('kubectl cp tip-wlan-opensync-gw-static-f795d45-ctb5z:/app/logs/mqttData.log mqttData.log') + # run a command + s.prompt() # match the prompt + print(s.before) # print everything before the prompt. + s.sendline() + s.logout() + #return "pass" + print(aws_host, aws_user) + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + k = paramiko.RSAKey.from_private_key_file('id_key.pem') + ssh.connect(aws_host, username=aws_user, pkey=k) + print("Connected") + scp = SCPClient(ssh.get_transport()) + scp.get("mqttData.log") + scp.close() + # Get the client Count + ClientCount = subprocess.getoutput( + 'grep \'{\"nodeID\"\' mqttData.log | grep clientList | tail -1 |cut -d \'=\' -f 3 | json_pp | grep macAddres | grep \'04:F0:21:55\' | tr -d , | sort | uniq | wc -l ') + print("client count =", ClientCount) + if (int(ClientCount) >= 1): + client.update_testrail(case_id=939, run_id=rid, status_id=1, + msg=ClientCount + ' Client/Clients Connected ') + else: + client.update_testrail(case_id=939, run_id=rid, status_id=5, + msg='No Client Connected') + except pxssh.ExceptionPxssh as e: + print("ALERT !!!!!! pxssh failed on login.") + print(e) + + +params = { + 'src': local_dir, + 'user2': ap_user, + 'host2': '10.10.10.207', + 'tgt': '/tmp/', + 'pwd': ap_pw, + 'opts': '' +} +apModel= "ecw5410" + + +url = 'https://tip.jfrog.io/artifactory/tip-wlan-ap-firmware/' +url = url + apModel +projId = client.get_project_id(project_name= 'WLAN') +print("TIP WLAN Project ID Is :", projId) + +rid = client.get_run_id(test_run_name= 'TIP-DEMO4') +print(rid) +Test: RunTest = RunTest() +Build: GetBuild = GetBuild() +''' +binary_fetch_result = Build.get_latest_image(url) +print("UPDATING TEST RAIL WITH TEST RESULT FOR CASE_ID 940: Download latest openwrt image from Jfrog") + +if binary_fetch_result == 'pass': + client.update_testrail(case_id=940, run_id=rid, status_id=1, msg='latest firmware downloaded') +else: + client.update_testrail(case_id=940, run_id=rid, status_id=5, msg='Firmware Download failed') + +sleep(10) +print("Upgrading AP with latest image downloaded") +ap_upgrade_result = openwrt_ap.ap_upgrade(**params) +sleep(10) +print("UPDATING TEST RAIL WITH TEST RESULT FOR CASE_ID 937") +sleep(10) +if ap_upgrade_result == 'pass': + client.update_testrail(case_id=937, run_id=rid, status_id=1, msg='AP upgraded with latest Firmware') +else: + client.update_testrail(case_id=937, run_id=rid, status_id=5, msg='Firmware upgrade failed in AP ') +print("Upgrading AWS Opensync gateway with latest docker image from Jfrog") +OpensyncGw_UpgResult = Build.run_opensyncgw_in_aws() +if OpensyncGw_UpgResult == 'pass': + client.update_testrail(case_id=936, run_id=rid, status_id=1, msg='Opensync GW upgraded with latest Firmware') +else: + client.update_testrail(case_id=936, run_id=rid, status_id=5, msg='Firmware upgrade failed in Opensync Gateway') +sleep(10) +''' +pprint.pprint(params) +ap_cert_result = openwrt_ap.apCopyCert(**params) +print("Executing TestCase 938: single Client Connectivity test") +openwrt_ap.restartGw(**params) +Test.TestCase_938(rid) + +print("Executing TestCase 941: Multi Client Connectivity test") +Test.TestCase_941(rid) +sleep(100) +print("Executing TestCase 939:Counting The number of Clients Connected from MQTT") +Test.TestCase_939(rid) + + + + From 40c7a2f75612d52559d8eb75579470772f40acae Mon Sep 17 00:00:00 2001 From: Logan Lipke Date: Wed, 10 Jun 2020 16:00:17 -0700 Subject: [PATCH 02/14] Added inheritance to CXProfile classes --- py-json/realm.py | 64 ++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/py-json/realm.py b/py-json/realm.py index e4e507c8..a7b0c246 100755 --- a/py-json/realm.py +++ b/py-json/realm.py @@ -13,9 +13,10 @@ from generic_cx import GenericCx class Realm(LFCliBase): - def __init__(self, lfclient_host="localhost", lfclient_port=8080, debug_on=True): - super().__init__(lfclient_host, lfclient_port, debug_on, _halt_on_error=True) + def __init__(self, lfclient_host="localhost", lfclient_port=8080, debug_=True): + super().__init__(lfclient_host, lfclient_port, debug_, _halt_on_error=True) # self.lfclient_url = "http://%s:%s" % (lfclient_host, lfclient_port) + self.debug = debug_ self.check_connect() # Returns json response from webpage of all layer 3 cross connects @@ -34,7 +35,7 @@ class Realm(LFCliBase): temp_map = LFUtils.portListToAliasMap(response) for k, v in temp_map.items(): if (v['port type'] == "WIFI-STA"): - sta_map[k] = v; + sta_map[k] = v temp_map.clear() del temp_map del response @@ -122,7 +123,7 @@ class Realm(LFCliBase): matched_map[port_eid] = record elif pattern.find("*") > 0: - match = re.search(r"^([^\*]+)[\*]$", pattern) + match = re.search(r"^([^\*]+)[*]$", pattern) if match.group(1): prefix = match.group(1) if debug_: @@ -151,26 +152,27 @@ class Realm(LFCliBase): return matched_map def new_station_profile(self): - station_prof = StationProfile(self.lfclient_url, debug=self.debugOn) + station_prof = StationProfile(self.lfclient_url, debug_=self.debug) return station_prof def new_l3_cx_profile(self): - cx_prof = L3CXProfile(self, self.lfclient_host, self.lfclient_port) + cx_prof = L3CXProfile(self.lfclient_host, self.lfclient_port, debug_=self.debug) return cx_prof def new_l4_cx_profile(self): - cx_prof = L4CXProfile(self, self.lfclient_host, self.lfclient_port) + cx_prof = L4CXProfile(self.lfclient_host, self.lfclient_port, debug_=self.debug) return cx_prof def new_generic_cx_profile(self): - cx_prof = GenericCx(self, self.lfclient_host, self.lfclient_port) + cx_prof = GenCXProfile(self.lfclient_host, self.lfclient_port, debug_=self.debug) return cx_prof -class L3CXProfile: - def __init__(self, lfclient_host, lfclient_port, debug=False): - self.lfclient_url = "http://%s:%s/" % (lfclient_host, lfclient_port) - self.debug = debug +class L3CXProfile(LFCliBase): + def __init__(self, lfclient_host, lfclient_port, debug_=False): + super().__init__(lfclient_host, lfclient_port, debug_, _halt_on_error=True) + self.lfclient_url = "http://%s:%s" % (lfclient_host, lfclient_port) + self.debug = debug_ def create(self, endp_type, side="a", ports=[], sleep_time=.5): post_data = [] @@ -211,8 +213,8 @@ class L3CXProfile: endp_side_b["port"] = port_name url = self.lfclient_url + "/cli-json/add_endp" - LFCliBase.json_post(url, endp_side_a) - LFCliBase.json_post(url, endp_side_b) + LFCliBase.json_post(self, url, endp_side_a) + LFCliBase.json_post(self, url, endp_side_b) time.sleep(sleep_time) data = { @@ -224,14 +226,15 @@ class L3CXProfile: post_data.append(data) for data in post_data: url = self.lfclient_url + "/cli-json/add_cx" - LFCliBase.json_post(url, data) + LFCliBase.json_post(self, url, data) time.sleep(sleep_time) -class L4CXProfile: - def __init__(self, lfclient_host, lfclient_port, debug=False): - self.lfclient_url = "http://%s:%s/" % (lfclient_host, lfclient_port) - self.debug = debug +class L4CXProfile(LFCliBase): + def __init__(self, lfclient_host, lfclient_port, debug_=False): + super().__init__(lfclient_host, lfclient_port, debug_, _halt_on_error=True) + self.lfclient_url = "http://%s:%s" % (lfclient_host, lfclient_port) + self.debug = debug_ self.url = "http://localhost/" self.requests_per_ten = 600 @@ -249,7 +252,7 @@ class L4CXProfile: "url": self.url } url = self.lfclient_url + "cli-json/add_l4_endp" - LFCliBase.json_post(url, data) + LFCliBase.json_post(self, url, data) time.sleep(sleep_time) data = { @@ -262,14 +265,17 @@ class L4CXProfile: for data in post_data: url = self.lfclient_url + "/cli-json/add_cx" - LFCliBase.json_post(url, data) + LFCliBase.json_post(self, url, data) time.sleep(sleep_time) -class GenCXProfile: - def __init__(self, lfclient_host, lfclient_port, debug=False): - self.lfclient_url = "http://%s:%s/" % (lfclient_host, lfclient_port) - self.debug = debug +class GenCXProfile(LFCliBase): + def __init__(self, lfclient_host, lfclient_port, debug_=False): + super().__init__(lfclient_host, lfclient_port, debug_, _halt_on_error=True) + self.lfclient_host = lfclient_host + self.lfclient_port = lfclient_port + self.lfclient_url = "http://%s:%s" % (lfclient_host, lfclient_port) + self.debug = debug_ self.type = "lfping" self.dest = "127.0.0.1" self.interval = 1 @@ -281,7 +287,7 @@ class GenCXProfile: for port_name in ports: gen_name = port_name + "_gen" gen_name2 = port_name + "_gen" - genl = GenericCx(lfclient_host=self.lfjson_host, lfclient_port=self.lfjson_port) + genl = GenericCx(lfclient_host=self.lfclient_host, lfclient_port=self.lfclient_port) genl.createGenEndp(gen_name, 1, 1, port_name, "gen_generic") genl.createGenEndp(gen_name2, 1, 1, port_name, "gen_generic") genl.setFlags(gen_name, "ClearPortOnStart", 1) @@ -300,7 +306,7 @@ class GenCXProfile: for data in post_data: url = self.lfclient_url + "/cli-json/add_cx" - LFCliBase.json_post(url, data) + LFCliBase.json_post(self, url, data) time.sleep(sleep_time) @@ -316,8 +322,8 @@ class GenCXProfile: # class StationProfile: def __init__(self, lfclient_url, ssid="NA", ssid_pass="NA", security="open", prefix="00000", mode=0, up=True, - dhcp=True, debug=False): - self.debug = debug + dhcp=True, debug_=False): + self.debug = debug_ self.lfclient_url = lfclient_url self.ssid = ssid self.ssid_pass = ssid_pass From 95b3545cc646141a71a74a5d487527e09d040a20 Mon Sep 17 00:00:00 2001 From: Logan Lipke Date: Wed, 10 Jun 2020 16:00:42 -0700 Subject: [PATCH 03/14] Added tests for CXProfile Classes --- py-json/realm_test.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/py-json/realm_test.py b/py-json/realm_test.py index 9668edd8..d23ca319 100755 --- a/py-json/realm_test.py +++ b/py-json/realm_test.py @@ -73,7 +73,7 @@ except Exception as x: exit(1) print(" - - - - TESTING - - - - - -") -exit(0) +#exit(0) print("** Existing vAPs **") try: @@ -95,14 +95,31 @@ except Exception as x: print("** Removing previous CXs **") -print("** Creating CXs **") +print("** Creating Layer 3 CXs **") try: - cxProfile = localrealm.new_cx_profile() + cxProfile = localrealm.new_l3_cx_profile() # set attributes of cxProfile - cxProfile.add_ports("A", "lf_udp", localrealm.find_ports_like("sta+")) - cxProfile.create() + cxProfile.create("A", "lf_udp", localrealm.find_ports_like("sta+")) except Exception as x: pprint(x) exit(1) +print("** Creating Layer 4 CXs **") +try: + cxProfile = localrealm.new_l4_cx_profile() + # set attributes of cxProfile + cxProfile.create(localrealm.find_ports_like("sta+")) +except Exception as x: + pprint(x) + exit(1) + +print("** Creating Generic CXs **") +try: + cxProfile = localrealm.new_generic_cx_profile() + # set attributes of cxProfile + cxProfile.create(localrealm.find_ports_like("sta+")) +except Exception as x: + pprint(x) + exit(1) # +exit(0) \ No newline at end of file From 17158fe4e0648e3441c7818b3493f08e42696335 Mon Sep 17 00:00:00 2001 From: Logan Lipke Date: Wed, 10 Jun 2020 16:01:10 -0700 Subject: [PATCH 04/14] Added debugOn attribute to StaConnect --- py-scripts/sta_connect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py-scripts/sta_connect.py b/py-scripts/sta_connect.py index 39dc3607..a5c51a1a 100755 --- a/py-scripts/sta_connect.py +++ b/py-scripts/sta_connect.py @@ -39,6 +39,7 @@ class StaConnect(LFCliBase): # that is py2 era syntax and will force self into the host variable, making you # very confused. super().__init__(host, port, _debug=_debugOn, _halt_on_error=_exit_on_error, _exit_on_fail=_exit_on_fail) + self.debugOn = _debugOn self.dut_security = "" self.dut_ssid = _dut_ssid self.dut_passwd = _dut_passwd From 3548fd13fb09f1e6ce3818ba520f13505c2c77c4 Mon Sep 17 00:00:00 2001 From: Logan Lipke Date: Wed, 10 Jun 2020 16:01:56 -0700 Subject: [PATCH 05/14] Renamed lfjson_host and lfjson_port to lfclient_host and lfclient_port --- py-json/LANforge/lfcli_base.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/py-json/LANforge/lfcli_base.py b/py-json/LANforge/lfcli_base.py index 2cd7caff..68c89c06 100644 --- a/py-json/LANforge/lfcli_base.py +++ b/py-json/LANforge/lfcli_base.py @@ -9,17 +9,18 @@ from LANforge.LFUtils import * class LFCliBase: - # do not use `super(LFCLiBase,self).__init__(self, host, port, _debugOn) + # do not use `super(LFCLiBase,self).__init__(self, host, port, _debug) # that is py2 era syntax and will force self into the host variable, making you # very confused. - def __init__(self, _lfjson_host, _lfjson_port, _debug=False, _halt_on_error=False, _exit_on_error=False, _exit_on_fail=False): + def __init__(self, _lfjson_host, _lfjson_port, _debug=False, _halt_on_error=False, _exit_on_error=False, + _exit_on_fail=False): self.fail_pref = "FAILED: " self.pass_pref = "PASSED: " - self.lfjson_host = _lfjson_host - self.lfjson_port = _lfjson_port - self.debugOn = _debug + self.lfclient_host = _lfjson_host + self.lfclient_port = _lfjson_port + self.debug = _debug self.haltOnError = _halt_on_error - self.lfclient_url = "http://%s:%s" % (self.lfjson_host, self.lfjson_port) + self.lfclient_url = "http://%s:%s" % (self.lfclient_host, self.lfclient_port) self.test_results = [] self.exit_on_error = _exit_on_error self.exit_on_fail = _exit_on_fail @@ -30,15 +31,15 @@ class LFCliBase: def json_post(self, _req_url, _data): json_response = None try: - lf_r = LFRequest.LFRequest(self.lfclient_url, _req_url, debug_=self.debugOn) + lf_r = LFRequest.LFRequest(self.lfclient_url, _req_url, debug_=self.debug) _data['suppress_preexec_cli'] = True _data['suppress_preexec_method'] = True lf_r.addPostData(_data) - if (self.debugOn): + if (self.debug): LANforge.LFUtils.debug_printer.pprint(_data) - json_response = lf_r.jsonPost(show_error=self.debugOn, debug=self.debugOn) + json_response = lf_r.jsonPost(show_error=self.debug, debug=self.debug) except Exception as x: - if self.debugOn or self.haltOnError: + if self.debug or self.haltOnError: print("jsonPost posted to %s" % _req_url) pprint(_data) print("Exception %s:" % x) @@ -49,17 +50,17 @@ class LFCliBase: return json_response def json_get(self, _req_url): - if self.debugOn: + if self.debug: print("URL: "+_req_url) json_response = None try: lf_r = LFRequest.LFRequest(self.lfclient_url, _req_url) - json_response = lf_r.getAsJson(self.debugOn) + json_response = lf_r.getAsJson(self.debug) #debug_printer.pprint(json_response) - if (json_response is None) and self.debugOn: + if (json_response is None) and self.debug: raise ValueError(json_response) except ValueError as ve: - if self.debugOn or self.haltOnError: + if self.debug or self.haltOnError: print("jsonGet asked for " + _req_url) print("Exception %s:" % ve) traceback.print_exception(ValueError, ve, ve.__traceback__, chain=True) @@ -79,7 +80,7 @@ class LFCliBase: # print("continuing...") def check_connect(self): - if self.debugOn: + if self.debug: print("Checking for LANforge GUI connection: %s" % self.lfclient_url) response = self.json_get("/") duration = 0 From e445ca5604ce96213513bc07a0843d59b72a0fb8 Mon Sep 17 00:00:00 2001 From: Logan Lipke Date: Wed, 10 Jun 2020 16:07:17 -0700 Subject: [PATCH 06/14] Renamed lfjson_host and lfjson_port to lfclient_host and lfclient_port --- connectTest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/connectTest.py b/connectTest.py index 7cd6a088..688164da 100755 --- a/connectTest.py +++ b/connectTest.py @@ -166,7 +166,7 @@ class ConnectTest(LFCliBase): time.sleep(.05) # create generic endpoints - genl = GenericCx(lfclient_host=self.lfjson_host, lfclient_port=self.lfjson_port) + genl = GenericCx(lfclient_host=self.lfclient_host, lfclient_port=self.lfclient_port) genl.createGenEndp("genTest1", 1, 1, staName, "gen_generic") genl.createGenEndp("genTest2", 1, 1, staName, "gen_generic") genl.setFlags("genTest1", "ClearPortOnStart", 1) @@ -468,9 +468,9 @@ class ConnectTest(LFCliBase): # ~class def main(): - lfjson_host = "localhost" - lfjson_port = 8080 - test = ConnectTest(lfjson_host, lfjson_port) + lfclient_host = "localhost" + lfclient_port = 8080 + test = ConnectTest(lfclient_host, lfclient_port) test.run() From a49aceee9d33297df402088c664c11effe7c0aba Mon Sep 17 00:00:00 2001 From: Jed Reynolds Date: Wed, 10 Jun 2020 16:29:55 -0700 Subject: [PATCH 07/14] sta_connect.py: updates main to assign item to station_names --- py-scripts/sta_connect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-scripts/sta_connect.py b/py-scripts/sta_connect.py index a5c51a1a..b53b51a8 100755 --- a/py-scripts/sta_connect.py +++ b/py-scripts/sta_connect.py @@ -476,7 +476,7 @@ Example: lfjson_port = args.port staConnect = StaConnect(lfjson_host, lfjson_port) - + staConnect.station_names = [ "sta0000" ] if args.user is not None: staConnect.user = args.user if args.passwd is not None: From cb1827ecac572d9b547995e4f44d4dcde7cefd86 Mon Sep 17 00:00:00 2001 From: Ben Greear Date: Wed, 10 Jun 2020 19:25:04 -0700 Subject: [PATCH 08/14] cicd: Make sta_connect print results to stdout when called as independent script. --- py-json/LANforge/lfcli_base.py | 3 +++ py-scripts/sta_connect.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/py-json/LANforge/lfcli_base.py b/py-json/LANforge/lfcli_base.py index 68c89c06..40935c6d 100644 --- a/py-json/LANforge/lfcli_base.py +++ b/py-json/LANforge/lfcli_base.py @@ -108,6 +108,9 @@ class LFCliBase: fail_messages = self.get_failed_result_list() return "\n".join(fail_messages) + def get_all_message(self): + return "\n".join(self.test_results) + def passes(self): pass_counter = 0 fail_counter = 0 diff --git a/py-scripts/sta_connect.py b/py-scripts/sta_connect.py index b53b51a8..79cf0477 100755 --- a/py-scripts/sta_connect.py +++ b/py-scripts/sta_connect.py @@ -499,9 +499,18 @@ Example: staConnect.dut_ssid = args.dut_ssid staConnect.run() + run_results = staConnect.get_result_list() + is_passing = staConnect.passes() + if is_passing == False: + print("FAIL: Some tests failed") + else: + print("PASS: All tests pass") + + print(staConnect.get_all_message()) + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 45b152e94b01e3dec3268698688ef255fe023da0 Mon Sep 17 00:00:00 2001 From: Jed Reynolds Date: Wed, 10 Jun 2020 21:08:54 -0700 Subject: [PATCH 09/14] Fixes wpa2 constant --- py-scripts/sta_connect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-scripts/sta_connect.py b/py-scripts/sta_connect.py index 79cf0477..8b618450 100755 --- a/py-scripts/sta_connect.py +++ b/py-scripts/sta_connect.py @@ -26,7 +26,7 @@ from realm import Realm OPEN="open" WEP="wep" WPA="wpa" -WPA2="wpa" +WPA2="wpa2" MODE_AUTO=0 class StaConnect(LFCliBase): From 9699e2741e116bb537c5305b76e3730c637b224d Mon Sep 17 00:00:00 2001 From: Jed Reynolds Date: Fri, 12 Jun 2020 12:55:17 -0700 Subject: [PATCH 10/14] vap_stations_example.pl demonstrates querying JSON api for /stations data --- json/vap_stations_example.pl | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100755 json/vap_stations_example.pl diff --git a/json/vap_stations_example.pl b/json/vap_stations_example.pl new file mode 100755 index 00000000..30c31002 --- /dev/null +++ b/json/vap_stations_example.pl @@ -0,0 +1,70 @@ +#!/usr/bin/perl -w +use strict; +use warnings; +use diagnostics; +use Carp; +$SIG{ __DIE__ } = sub { Carp::confess( @_ ) }; +$SIG{ __WARN__ } = sub { Carp::confess( @_ ) }; + +# Un-buffer output +$| = 1; +use Getopt::Long; +use JSON::XS; +use HTTP::Request; +use LWP; +use LWP::UserAgent; +use Data::Dumper; +use Time::HiRes qw(usleep); +use JSON; +use lib '/home/lanforge/scripts'; +use LANforge::JsonUtils qw(logg err json_request get_links_from get_thru json_post get_port_names flatten_list); + +package main; +# Default values for ye ole cmd-line args. +our $Resource = 1; +our $quiet = "yes"; +our $Host = "localhost"; +our $Port = 8080; +our $HostUri = "http://$Host:$Port"; +our $Web = LWP::UserAgent->new; +our $Decoder = JSON->new->utf8; +our $ssid; +our $security; +our $passphrase; + +my $usage = qq("$0 --host {ip or hostname} # connect to this + --port {port number} # defaults to 8080 +); + + +my $des_resource = 1; + +GetOptions +( + 'host=s' => \$::Host, + 'port=i' => \$::Port, + 'resource=i' => \$des_resource +) || (print($usage) && exit(1)); + +$::HostUri = "http://$Host:$Port"; + +my $uri = "/stations/list"; +my $rh = json_request($uri); +my $ra_links = get_links_from($rh, 'stations'); +# print(Dumper($ra_links)); + +my @attribs = ("ap", "signal", "tx rate", "rx rate", "capabilities"); +for my $sta_uri (@$ra_links) { + my $with_fields = "$sta_uri?fields=station+bssid,capabilities,rx+rate,tx+rate,signal,ap"; + $rh = json_request($with_fields); + #print(Dumper($rh)); + #print(Dumper($rh->{station})); + print("Station ", $rh->{station}->{'station bssid'}, "\n"); + for my $k (@attribs) { + print(" $k: ".$rh->{station}->{$k}."\n"); + } + print("\n"); +} + + + From d59f61b4134bc585a3dd0312521bb07297b4a131 Mon Sep 17 00:00:00 2001 From: Jed Reynolds Date: Fri, 12 Jun 2020 13:28:21 -0700 Subject: [PATCH 11/14] vap_stations_example.py: shows how to query /stations uri --- py-scripts/vap_stations_example.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100755 py-scripts/vap_stations_example.py diff --git a/py-scripts/vap_stations_example.py b/py-scripts/vap_stations_example.py new file mode 100755 index 00000000..ba658f38 --- /dev/null +++ b/py-scripts/vap_stations_example.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import sys +if sys.version_info[0] != 3: + print("This script requires Python 3") + exit(1) +if 'py-json' not in sys.path: + sys.path.append('../py-json') +import traceback + +from LANforge import LFUtils +from LANforge.LFUtils import * +from LANforge.lfcli_base import LFCliBase +from generic_cx import GenericCx + +mgrURL = "http://localhost:8080/" +staName = "sta0" +staNameUri = "port/1/1/" + staName + + +class VapStations(LFCliBase): + def __init__(self, lfhost, lfport): + super().__init__(lfhost, lfport, _debug=False) + super().check_connect() + + def run(self): + list_resp = self.json_get("/stations/list") + list_map = self.response_list_to_map(list_resp, 'stations') + # pprint.pprint(list_map) + + attribs = ["ap", "capabilities", "tx rate", "rx rate", "signal"] + for eid,record in list_map.items(): + # print("mac: %s" % mac) + mac = record["station bssid"] + station_resp = self.json_get("/stations/%s?fields=capabilities,tx+rate,rx+rate,signal,ap" % mac) + print("Station %s:" %mac) + #pprint.pprint(station_resp) + for attrib in attribs: + print(" %s: %s" % (attrib, station_resp["station"][attrib])) + + +def main(): + vapsta_test = VapStations("localhost", 8080) + vapsta_test.run() + +if __name__ == '__main__': + main() From e54119758e00e9052c39e31b199a3ed91d8b2c8d Mon Sep 17 00:00:00 2001 From: Jed Reynolds Date: Fri, 12 Jun 2020 13:29:47 -0700 Subject: [PATCH 12/14] lfcli_base.py: repose_list_to_map is a more general method of doing LFUtils.portListToAliasMap() --- py-json/LANforge/lfcli_base.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/py-json/LANforge/lfcli_base.py b/py-json/LANforge/lfcli_base.py index 40935c6d..3d1090f6 100644 --- a/py-json/LANforge/lfcli_base.py +++ b/py-json/LANforge/lfcli_base.py @@ -69,6 +69,38 @@ class LFCliBase: return json_response + @staticmethod + def response_list_to_map(json_list, key, debug_=False): + reverse_map = {} + if (json_list is None) or (len(json_list) < 1): + if debug_: + print("response_list_to_map: no json_list provided") + raise ValueError("response_list_to_map: no json_list provided") + return reverse_map + + json_interfaces = json_list + if key in json_list: + json_interfaces = json_list[key] + + for record in json_interfaces: + if len(record.keys()) < 1: + continue + record_keys = record.keys() + k2 = "" + # we expect one key in record keys, but we can't expect [0] to be populated + json_entry = None + for k in record_keys: + k2 = k + json_entry = record[k] + # skip uninitialized port records + if k2.find("Unknown") >= 0: + continue + port_json = record[k2] + reverse_map[k2] = json_entry + + return reverse_map + + def error(self, exception): # print("lfcli_base error: %s" % exception) pprint.pprint(exception) From 85bf11c9c2c3aa5a3741ac7e2175257ffda61abc Mon Sep 17 00:00:00 2001 From: Ben Greear Date: Fri, 12 Jun 2020 15:45:36 -0700 Subject: [PATCH 13/14] testrails: Allow submitting externally generated test results into testrails. This way, some other tool (ie, LANforge performance & stability tests automation) can create results that can be put into testrails. This external automation will not need direct access to test rails (user-account info, etc) Code is not tested, plz comment out if it breaks something. --- py-scripts/cicd_TipIntegration.py | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/py-scripts/cicd_TipIntegration.py b/py-scripts/cicd_TipIntegration.py index eb8261d9..02651f2c 100644 --- a/py-scripts/cicd_TipIntegration.py +++ b/py-scripts/cicd_TipIntegration.py @@ -17,6 +17,13 @@ import paramiko from scp import SCPClient import pprint from pprint import pprint +from os import listdir +import re + +# For finding files +# https://stackoverflow.com/questions/3207219/how-do-i-list-all-files-of-a-directory +import glob +external_results_dir=/var/tmp/lanforge local_dir=os.getenv('LOG_DIR') print("Local Directory where all files will be copied and logged", local_dir) @@ -388,6 +395,48 @@ class RunTest: client.update_testrail(case_id=941, run_id=rid, status_id=5, msg='client connectivity to 5GHZ OPEN SSID is Failed') + # Check for externally run test case results. + def TestCase_LF_External(self, rid): + #https://stackoverflow.com/questions/3207219/how-do-i-list-all-files-of-a-directory + results = glob.glob("%s/*_CICD_RESULTS.txt"%external_results_dir) + for r in results: + rfile = open(r, 'r') + lines = rfile.readlines() + + # File contents looks something like: + #CASE_ID 9999 + #RUN_ID 15 + #STATUS 1 + #MSG Test passed nicely + #MSG Build ID: deadbeef + #MSG Results: http://cicd.telecominfraproject.com + + _case_id = -1 + _status_id = 1 # Default to pass + _msg = "" + _rid = rid + + for line in Lines: + m = re.search(r'(\S+) (.*)', line) + k = m.group(0); + v = m.group(1); + + if k == "CASE_ID": + _case_id = v + if k == "RUN_ID": + _rid = v + if k == "STATUS": + _status_id = v + if k == "MSG": + if _msg == "": + _msg == v + else: + _msg += "\n" + _msg += v + if _case_id != -1: + client.update_testrail(case_id=_case_id, run_id=_rid, status_id=_status_id, msg=_msg) + os.unlink(r) + def TestCase_939(self, rid): ''' Client Count in MQTT Log''' try: From f076c820623ec55a7c8e0d8e7856445e5c2440a6 Mon Sep 17 00:00:00 2001 From: Ben Greear Date: Fri, 12 Jun 2020 15:47:51 -0700 Subject: [PATCH 14/14] kpi: Add initial code to create testrails data file Will allow importing results into testrails. --- gui/kpi.java | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gui/kpi.java b/gui/kpi.java index d03b655d..53d175ac 100644 --- a/gui/kpi.java +++ b/gui/kpi.java @@ -38,6 +38,7 @@ import java.nio.file.*; public class kpi { String lc_osname; String home_dir; + static final String out_sep = "\t"; static final String in_sep = "\t"; @@ -118,11 +119,19 @@ public class kpi { public void work(String[] args) { String dir = null; + String results_url = ""; + String caseid = ""; for (int i = 0; i" + i + "" + run.getName() + "" + run.getDate() + "" + run.getDutHwVer() + "" + run.getDutSwVer() + "" + run.getDutModelNum() + "" + greenTd(run.getPass() + "") + redTd(run.getFail() + "") + logs_str + "\n"); + if (i == (runs.size() - 1)) { // Last run int png_row_count = 0; @@ -589,6 +599,34 @@ public class kpi { if ((!needs_tr) && pngs.length() > 0) { pngs.append("\n"); } + + String testrails_msg = ("MSG Run: " + run.getName() + " Date: " + run.getDate() + " DUT-HW: " + run.getDutHwVer() + "DUT-SW: " + run.getDutSwVer() + + " Passed: " + run.getPass() + " Fail: " + run.getFail() + + " Log-Bugs: " + run.getLogBugs() + " Log-Warnings: " + run.getLogWarnings() + + " Log-Crashes: " + run.getLogCrashes() + " Log-Restarting: " + run.getLogRestarting()); + + if (!caseid.equals("")) { + try { + // Create file for propagating results into external tool + String ofile = dir + File.separator + "testrails.txt"; + BufferedWriter bw = new BufferedWriter(new FileWriter(ofile)); + + bw.write("CASE_ID " + caseid); + bw.write("RUN_ID " + i); + bw.write(System.lineSeparator()); + bw.write(testrails_msg); + bw.write(System.lineSeparator()); + bw.write("MSG URL: " + results_url); + bw.write(System.lineSeparator()); + + bw.close(); + + System.out.println("See testrails results: " + ofile); + } + catch (Exception eee) { + eee.printStackTrace(); + } + } } runs_rows.append(row_text);