- initial import

This commit is contained in:
2023-08-09 19:53:31 +03:00
commit b701793923
73 changed files with 5835 additions and 0 deletions

349
src/utils_qualtest.py Normal file
View 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