- initial import
This commit is contained in:
349
src/utils_qualtest.py
Normal file
349
src/utils_qualtest.py
Normal file
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/python
|
||||
import utils
|
||||
import re
|
||||
import subprocess
|
||||
import typing
|
||||
import csv
|
||||
import platform
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib
|
||||
import uuid
|
||||
import time
|
||||
import requests
|
||||
|
||||
from socket import timeout
|
||||
from crontab import CronTab
|
||||
|
||||
|
||||
start_system_time = time.time()
|
||||
start_monotonic_time = time.monotonic()
|
||||
|
||||
# Error report produced by this function has to be updated with 'task_name' & 'phone_name' keys
|
||||
def build_error_report(endtime: int, reason: str):
|
||||
r = dict()
|
||||
r["id"] = uuid.uuid1().urn[9:]
|
||||
r["duration"] = 0
|
||||
r["endtime"] = endtime
|
||||
r["mos_pvqa"] = 0.0
|
||||
r["mos_aqua"] = 0.0
|
||||
r["mos_network"] = 0.0
|
||||
r["r_factor"] = 0
|
||||
r["percents_aqua"] = 0.0
|
||||
r["error"] = reason
|
||||
return r
|
||||
|
||||
|
||||
class TaskList:
|
||||
tasks: list = []
|
||||
|
||||
def __init__(self):
|
||||
self.tasks = []
|
||||
|
||||
|
||||
# Merges incoming task list to existing one
|
||||
# It preserves existing schedules
|
||||
# New items are NOT scheduled automatically
|
||||
def merge_with(self, tasklist) -> bool:
|
||||
changed = False
|
||||
if tasklist.tasks is None:
|
||||
return True
|
||||
|
||||
# Iterate all tasks, see if task with the same name exists already
|
||||
# Copy all keys, but keep existing ones
|
||||
for new_task in tasklist.tasks:
|
||||
# Find if this task exists already
|
||||
existing_task = self.find_task_by_name(new_task["name"])
|
||||
|
||||
# If task is found - copy all items to it.
|
||||
# It is required as task can hold schedule items already
|
||||
# Bad idea to copy tasks itself.
|
||||
if existing_task is not None:
|
||||
# Check if scheduled time point has to be removed (if cron string changed)
|
||||
if new_task["schedule"] != existing_task["schedule"] and "scheduled_time" in existing_task:
|
||||
del existing_task["scheduled_time"]
|
||||
|
||||
|
||||
# Finally copy new values
|
||||
for key, value in new_task.items():
|
||||
if existing_task[key] != value:
|
||||
existing_task[key] = value
|
||||
changed = True
|
||||
else:
|
||||
# Copy new task to list
|
||||
self.tasks.extend([new_task])
|
||||
changed = True
|
||||
|
||||
# Check if old tasks are here... And delete them
|
||||
for existing_task in self.tasks:
|
||||
new_task = self.find_task_by_name(existing_task["name"])
|
||||
if new_task is None:
|
||||
self.tasks.remove(existing_task)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def schedule(self):
|
||||
# Remove items without schedule before
|
||||
self.tasks = [t for t in self.tasks if len(t['schedule']) > 0]
|
||||
|
||||
# https://crontab.guru is good for crontab strings generation
|
||||
# Use monotonic time source!
|
||||
current_time = time.monotonic()
|
||||
for task in self.tasks:
|
||||
if 'scheduled_time' not in task and 'schedule' in task:
|
||||
# No schedule flag, so time to schedule
|
||||
try:
|
||||
cron_string = task['schedule'].strip()
|
||||
if cron_string == '* * * * *':
|
||||
task['scheduled_time'] = time.monotonic() - 0.001 # To ensure further comparison will not be affected by precision errors
|
||||
else:
|
||||
cron = CronTab(task['schedule'])
|
||||
task['scheduled_time'] = current_time + cron.next(default_utc=True)
|
||||
|
||||
# Just to help in further log reading & debugging - show the scheduled time in readable form
|
||||
task['scheduled_time_str'] = time.ctime(task['scheduled_time'] - start_monotonic_time + start_system_time)
|
||||
except:
|
||||
utils.log_error("Error", sys.exc_info()[0])
|
||||
|
||||
# Remove non scheduled items
|
||||
self.tasks = [t for t in self.tasks if 'scheduled_time' in t]
|
||||
|
||||
# Sort everything
|
||||
self.tasks = sorted(self.tasks, key=lambda t: t["scheduled_time"])
|
||||
|
||||
|
||||
# Returns None if failed
|
||||
def find_task_by_name(self, name):
|
||||
for t in self.tasks:
|
||||
if t["name"] == name:
|
||||
return t
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def ParseAttributes(t: str) -> dict:
|
||||
result: dict = dict()
|
||||
|
||||
for l in t.split('\n'):
|
||||
tokens = l.strip().split('=')
|
||||
if len(tokens) == 2:
|
||||
result[tokens[0].strip()] = tokens[1].strip()
|
||||
return result
|
||||
|
||||
|
||||
class Phone:
|
||||
identifier: int = 0
|
||||
name: str = ""
|
||||
role: str = ""
|
||||
attributes: dict = ""
|
||||
audio_id: int = 0
|
||||
|
||||
def __init__(self):
|
||||
self.identifier = 0
|
||||
self.name = ""
|
||||
self.role = ""
|
||||
self.attributes = dict()
|
||||
self.audio_id = 0
|
||||
|
||||
|
||||
class QualtestBackend:
|
||||
address: str
|
||||
instance: str
|
||||
|
||||
def __init__(self):
|
||||
self.address = ""
|
||||
self.instance = ""
|
||||
self.__phone = None
|
||||
|
||||
|
||||
@property
|
||||
def phone(self) -> Phone:
|
||||
return self.__phone
|
||||
|
||||
|
||||
def preload(self):
|
||||
self.__phone = self.load_phone()
|
||||
|
||||
|
||||
def upload_report(self, report, files) -> str:
|
||||
# UUID string as result
|
||||
result = None
|
||||
|
||||
# Log about upload attempt
|
||||
utils.log_verbose(f"Uploading to {self.address} files {files} and report: {json.dumps(report, indent=4)}")
|
||||
|
||||
# POST will be sent to args.qualtest_server with args.qualtest_instance ID
|
||||
json_content = json.dumps(report, indent=4).encode('utf8')
|
||||
|
||||
# Find URL for uploading
|
||||
url = utils.join_host_and_path(self.address, "/probes/")
|
||||
try:
|
||||
# Step 1 - upload result record
|
||||
req = urllib.request.Request(url,
|
||||
data=json_content,
|
||||
headers={'content-type': 'application/json'})
|
||||
response = urllib.request.urlopen(req, timeout=utils.NETWORK_TIMEOUT)
|
||||
result = response.read().decode('utf8')
|
||||
utils.log_verbose(f"Response (probe ID): {result}")
|
||||
utils.log_verbose(f"Upload to {self.address} finished.")
|
||||
|
||||
except Exception as e:
|
||||
utils.log_error(f"Upload to {self.address} finished with error.", err=e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def upload_audio(self, probe_id, path_recorded):
|
||||
result = False
|
||||
|
||||
# Log about upload attempt
|
||||
utils.log_verbose(f"Uploading to {self.address} audio {path_recorded}")
|
||||
|
||||
# Find URL for uploading
|
||||
url = utils.join_host_and_path(self.address, "/upload_audio/")
|
||||
try:
|
||||
files = {'file': (os.path.basename(path_recorded), open(path_recorded, 'rb')),
|
||||
'probe_id': (None, probe_id),
|
||||
'audio_kind': (None, '1'),
|
||||
'audio_name': (None, os.path.basename(path_recorded))}
|
||||
|
||||
# values = {'probe_id': probe_id}
|
||||
response = requests.post(url, files=files, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.status_code != 200:
|
||||
utils.log_error(f"Upload audio to {self.address} finished with error {response.status_code}", None)
|
||||
else:
|
||||
utils.log_verbose(f"Response (audio ID): {response.text}")
|
||||
utils.log_verbose(f"Upload audio to {self.address} finished.")
|
||||
result = True
|
||||
except Exception as e:
|
||||
utils.log_error(f"Upload audio to {self.address} finished with error.", err=e)
|
||||
|
||||
return result
|
||||
|
||||
def load_tasks(self) -> TaskList:
|
||||
try:
|
||||
# Build query for both V1 & V2 API
|
||||
instance = urllib.parse.urlencode({"phone_id": self.instance, "phone_name": self.instance})
|
||||
|
||||
# Find URL
|
||||
url = utils.join_host_and_path(self.address, "/tasks/?") + instance
|
||||
|
||||
# Get response from server
|
||||
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.getcode() != 200:
|
||||
utils.log_error("Failed to get task list. Error code: %s" % response.getcode())
|
||||
return None
|
||||
|
||||
result = TaskList()
|
||||
response_content = response.read().decode()
|
||||
result.tasks = json.loads(response_content)
|
||||
return result
|
||||
|
||||
except Exception as err:
|
||||
utils.log_error("Exception when fetching task list: {0}".format(err))
|
||||
return None
|
||||
|
||||
|
||||
def load_phone(self) -> dict:
|
||||
try:
|
||||
# Build query for both V1 & V2 API
|
||||
instance = urllib.parse.urlencode({"phone_id": self.instance, "phone_name": self.instance})
|
||||
|
||||
# Find URL
|
||||
url = utils.join_host_and_path(self.address, "/phones/?") + instance
|
||||
|
||||
# Get response from server
|
||||
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.getcode() != 200:
|
||||
utils.log_error("Failed to get task list. Error code: %s" % response.getcode())
|
||||
return None
|
||||
|
||||
result: Phone = Phone()
|
||||
phones = json.loads(response.read().decode())
|
||||
if len(phones) == 0:
|
||||
return result
|
||||
|
||||
phone = phones[0]
|
||||
|
||||
attr_dict = dict()
|
||||
attributes_string = phone['attributes']
|
||||
attributes_lines = attributes_string.split('\n')
|
||||
for l in attributes_lines:
|
||||
parts = l.split('=')
|
||||
if len(parts) == 2:
|
||||
p0: str = parts[0]
|
||||
p1: str = parts[1]
|
||||
attr_dict[p0.strip()] = p1.strip()
|
||||
|
||||
# Fix received attributes
|
||||
if 'stun_server' in attr_dict:
|
||||
attr_dict['sip_stunserver'] = attr_dict.pop('stun_server')
|
||||
if 'transport' in attr_dict:
|
||||
attr_dict['sip_transport'] = attr_dict.pop('transport')
|
||||
|
||||
if 'sip_secure' not in attr_dict:
|
||||
attr_dict['sip_secure'] = False
|
||||
if 'sip_useproxy' not in attr_dict:
|
||||
attr_dict['sip_useproxy'] = True
|
||||
|
||||
result.attributes = attr_dict
|
||||
result.identifier = phone['id']
|
||||
result.name = phone['instance']
|
||||
result.role = phone['type']
|
||||
result.audio_id = phone['audio_id']
|
||||
|
||||
return result
|
||||
|
||||
except Exception as err:
|
||||
utils.log_error("Exception when fetching task list: {0}".format(err))
|
||||
return dict()
|
||||
|
||||
|
||||
def load_audio(self, audio_id: int, output_path: str):
|
||||
utils.log(f'Loading audio with ID: {audio_id}')
|
||||
try:
|
||||
# Build query for both V1 & V2 API
|
||||
params = urllib.parse.urlencode({"audio_id": audio_id})
|
||||
|
||||
# Find URL
|
||||
url = utils.join_host_and_path(self.address, "/play_audio/?") + params
|
||||
|
||||
# Get response from server
|
||||
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.getcode() != 200:
|
||||
utils.log_error("Failed to get audio. Error code: %s" % response.getcode())
|
||||
return False
|
||||
|
||||
audio_content = response.read()
|
||||
with open (output_path, 'wb') as f:
|
||||
f.write(audio_content)
|
||||
|
||||
return True
|
||||
except Exception as err:
|
||||
utils.log_error("Exception when fetching list: {0}".format(err))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def load_task(self, task_name: str) -> dict:
|
||||
try:
|
||||
params = urllib.parse.urlencode({'task_name': task_name})
|
||||
url = utils.join_host_and_path(self.address, "/tasks/?" + params)
|
||||
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
|
||||
if response.getcode() != 200:
|
||||
utils.log_error(f'Failed to get task info. Error code: {response.getcode()}')
|
||||
return None
|
||||
|
||||
task_list = json.loads(response.read().decode())
|
||||
if len(task_list) > 0:
|
||||
return task_list[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as err:
|
||||
utils.log_error(f'Exception when fetching task info: {err}')
|
||||
return None
|
||||
Reference in New Issue
Block a user