syncloud-lib 333__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syncloud_lib-333.dist-info/METADATA +21 -0
- syncloud_lib-333.dist-info/RECORD +31 -0
- syncloud_lib-333.dist-info/WHEEL +5 -0
- syncloud_lib-333.dist-info/licenses/LICENSE +675 -0
- syncloud_lib-333.dist-info/top_level.txt +1 -0
- syncloudlib/__init__.py +0 -0
- syncloudlib/application/__init__.py +0 -0
- syncloudlib/application/config.py +9 -0
- syncloudlib/application/connection.py +40 -0
- syncloudlib/application/paths.py +9 -0
- syncloudlib/application/ports.py +9 -0
- syncloudlib/application/service.py +6 -0
- syncloudlib/application/storage.py +9 -0
- syncloudlib/application/urls.py +11 -0
- syncloudlib/application/users.py +5 -0
- syncloudlib/error.py +6 -0
- syncloudlib/fs.py +44 -0
- syncloudlib/http.py +28 -0
- syncloudlib/integration/__init__.py +0 -0
- syncloudlib/integration/conftest.py +247 -0
- syncloudlib/integration/device.py +111 -0
- syncloudlib/integration/hosts.py +9 -0
- syncloudlib/integration/installer.py +78 -0
- syncloudlib/integration/loop.py +65 -0
- syncloudlib/integration/screenshots.py +7 -0
- syncloudlib/integration/selenium_wrapper.py +127 -0
- syncloudlib/integration/ssh.py +51 -0
- syncloudlib/json/__init__.py +0 -0
- syncloudlib/json/convertible.py +114 -0
- syncloudlib/linux.py +15 -0
- syncloudlib/logger.py +54 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from syncloudlib.integration.ssh import run_ssh
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LosetupEntry:
|
|
6
|
+
def __init__(self, device, file):
|
|
7
|
+
self.file = file
|
|
8
|
+
self.device = device
|
|
9
|
+
|
|
10
|
+
def is_deleted(self):
|
|
11
|
+
return '(deleted)' in self.file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MountEntry:
|
|
15
|
+
def __init__(self, device):
|
|
16
|
+
self.device = device
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def loop_device_cleanup(host, dev_file, password):
|
|
20
|
+
print('cleanup')
|
|
21
|
+
for mount in parse_mount(run_ssh(host, 'mount', password=password)):
|
|
22
|
+
if dev_file == mount.device:
|
|
23
|
+
run_ssh(host, 'umount {0}'.format(mount.device), throw=False, password=password)
|
|
24
|
+
|
|
25
|
+
for loop in parse_losetup(run_ssh(host, 'losetup', password=password)):
|
|
26
|
+
if loop.file == dev_file or loop.is_deleted():
|
|
27
|
+
run_ssh(host, 'losetup -d {0}'.format(loop.device), throw=False, password=password)
|
|
28
|
+
|
|
29
|
+
run_ssh(host, 'losetup', password=password)
|
|
30
|
+
|
|
31
|
+
for loop in run_ssh(host, 'dmsetup ls', password=password).splitlines():
|
|
32
|
+
if 'loop0p1' in loop:
|
|
33
|
+
run_ssh(host, 'sudo dmsetup remove loop0p1', password=password)
|
|
34
|
+
if 'loop0p2' in loop:
|
|
35
|
+
run_ssh(host, 'sudo dmsetup remove loop0p2', password=password)
|
|
36
|
+
|
|
37
|
+
run_ssh(host, 'rm -rf {0}'.format(dev_file), throw=False, password=password)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_losetup(output):
|
|
41
|
+
entries = []
|
|
42
|
+
for line in output.splitlines():
|
|
43
|
+
match = re.match(r'(.*[0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+(.*)\s+([0-9]+)\s+([0-9]+)', line.strip())
|
|
44
|
+
if match:
|
|
45
|
+
entry = LosetupEntry(match.group(1).strip(), match.group(6).strip())
|
|
46
|
+
entries.append(entry)
|
|
47
|
+
return entries
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_mount(output):
|
|
51
|
+
entries = []
|
|
52
|
+
for line in output.splitlines():
|
|
53
|
+
match = re.match(r'(.*)\son\s.*', line.strip())
|
|
54
|
+
if match:
|
|
55
|
+
entry = MountEntry(match.group(1).strip())
|
|
56
|
+
entries.append(entry)
|
|
57
|
+
return entries
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def loop_device_add(host, fs, dev_file, password):
|
|
61
|
+
print('adding loop device')
|
|
62
|
+
run_ssh(host, 'dd if=/dev/zero bs=1M count=10 of={0}'.format(dev_file), password=password)
|
|
63
|
+
loop = run_ssh(host, 'losetup -f --show {0}'.format(dev_file), password=password)
|
|
64
|
+
run_ssh(host, 'mkfs.{0} {1}'.format(fs, loop), password=password)
|
|
65
|
+
return loop
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from os.path import join
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def screenshots(driver, screenshot_dir, name):
|
|
5
|
+
driver.get_screenshot_as_file(join(screenshot_dir, '{0}.png'.format(name)))
|
|
6
|
+
with open(join(screenshot_dir, '{0}.html.log'.format(name)), "w") as f:
|
|
7
|
+
f.write(str(driver.page_source.encode("utf-8")))
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from retry import retry
|
|
4
|
+
|
|
5
|
+
from syncloudlib.integration.screenshots import screenshots
|
|
6
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
7
|
+
from selenium.webdriver.common.by import By
|
|
8
|
+
from selenium.webdriver.support import expected_conditions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SeleniumWrapper:
|
|
12
|
+
def __init__(self, driver, ui_mode, screenshot_dir, app_domain, timeout, browser):
|
|
13
|
+
self.app_domain = app_domain
|
|
14
|
+
self.screenshot_dir = screenshot_dir
|
|
15
|
+
self.ui_mode = ui_mode
|
|
16
|
+
self.driver = driver
|
|
17
|
+
self.browser = browser
|
|
18
|
+
self.wait_driver = WebDriverWait(self.driver, timeout)
|
|
19
|
+
|
|
20
|
+
def find_by_xpath(self, xpath):
|
|
21
|
+
return self.find_by(By.XPATH, xpath)
|
|
22
|
+
|
|
23
|
+
def find_by_name(self, name):
|
|
24
|
+
return self.find_by(By.NAME, name)
|
|
25
|
+
|
|
26
|
+
def find_by_id(self, field_id):
|
|
27
|
+
return self.find_by(By.ID, field_id)
|
|
28
|
+
|
|
29
|
+
def find_by_css(self, css):
|
|
30
|
+
return self.find_by(By.CSS_SELECTOR, css)
|
|
31
|
+
|
|
32
|
+
def find_by(self, by, value, parent=None):
|
|
33
|
+
if not parent:
|
|
34
|
+
parent = self.driver
|
|
35
|
+
#self.wait_or_screenshot(expected_conditions.visibility_of_element_located((by, value)))
|
|
36
|
+
self.wait_or_screenshot(expected_conditions.presence_of_element_located((by, value)))
|
|
37
|
+
try:
|
|
38
|
+
return parent.find_element(by, value)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
self.screenshot('exception', True)
|
|
41
|
+
|
|
42
|
+
def invisible_by(self, by, value):
|
|
43
|
+
self.wait_or_screenshot(expected_conditions.invisibility_of_element_located((by, value)))
|
|
44
|
+
|
|
45
|
+
def click_by(self, by, value):
|
|
46
|
+
#self.wait_or_screenshot(expected_conditions.element_to_be_clickable((by, value)))
|
|
47
|
+
self.wait_or_screenshot(expected_conditions.presence_of_element_located((by, value)))
|
|
48
|
+
try:
|
|
49
|
+
self.driver.find_element(by, value).click()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.screenshot('exception', True)
|
|
52
|
+
|
|
53
|
+
def clickable_by(self, by, value):
|
|
54
|
+
self.wait_or_screenshot(expected_conditions.element_to_be_clickable((by, value)))
|
|
55
|
+
try:
|
|
56
|
+
return self.driver.find_element(by, value)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.screenshot('exception', True)
|
|
59
|
+
|
|
60
|
+
def present_by(self, by, value):
|
|
61
|
+
self.wait_or_screenshot(expected_conditions.presence_of_element_located((by, value)))
|
|
62
|
+
try:
|
|
63
|
+
return self.driver.find_element(by, value)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.screenshot('exception', True)
|
|
66
|
+
|
|
67
|
+
def exists_by(self, by, value, timeout=10):
|
|
68
|
+
driver = WebDriverWait(self.driver, timeout)
|
|
69
|
+
cond = expected_conditions.visibility_of_element_located((by, value))
|
|
70
|
+
try:
|
|
71
|
+
driver.until(cond)
|
|
72
|
+
return True
|
|
73
|
+
except Exception as _:
|
|
74
|
+
self.screenshot('exception', False)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
@retry(exceptions=Exception, tries=3, delay=1, backoff=2)
|
|
78
|
+
def wait_or_screenshot(self, method, throw=True):
|
|
79
|
+
try:
|
|
80
|
+
self.wait_driver.until(method)
|
|
81
|
+
return True
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.screenshot('exception', throw)
|
|
84
|
+
if throw:
|
|
85
|
+
raise e
|
|
86
|
+
else:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def screenshot(self, name, throw=True):
|
|
90
|
+
retries = 5
|
|
91
|
+
retry = 0
|
|
92
|
+
while True:
|
|
93
|
+
try:
|
|
94
|
+
screenshots(self.driver, self.screenshot_dir, '{}-{}'.format(name, self.ui_mode))
|
|
95
|
+
break
|
|
96
|
+
except Exception as e:
|
|
97
|
+
if retry >= retries:
|
|
98
|
+
if throw:
|
|
99
|
+
raise
|
|
100
|
+
else:
|
|
101
|
+
return
|
|
102
|
+
retry += 1
|
|
103
|
+
time.sleep(1)
|
|
104
|
+
print('retrying screenshot {0}'.format(retry))
|
|
105
|
+
|
|
106
|
+
def open_app(self, path=''):
|
|
107
|
+
self.driver.get("https://{0}{1}".format(self.app_domain, path))
|
|
108
|
+
|
|
109
|
+
def log(self):
|
|
110
|
+
if self.browser != "chrome":
|
|
111
|
+
print("browser logs are only supported in chrome")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
print("browser log")
|
|
115
|
+
for entry in self.driver.get_log('browser'):
|
|
116
|
+
print(entry)
|
|
117
|
+
|
|
118
|
+
@retry(exceptions=Exception, tries=10, delay=1, backoff=2)
|
|
119
|
+
def element_by_js(self, js):
|
|
120
|
+
try:
|
|
121
|
+
elem = self.driver.execute_script('return ' + js)
|
|
122
|
+
self.driver.execute_script("arguments[0].scrollIntoView();", elem)
|
|
123
|
+
return elem
|
|
124
|
+
except Exception:
|
|
125
|
+
self.screenshot('exception')
|
|
126
|
+
raise
|
|
127
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from subprocess import check_output, STDOUT, CalledProcessError
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_scp(command, throw=True, debug=True, password='syncloud', retries=0, sleep=1, port=22):
|
|
7
|
+
retry = 0
|
|
8
|
+
while True:
|
|
9
|
+
try:
|
|
10
|
+
return _run_command('scp -P {0} -o StrictHostKeyChecking=no {1}'.format(port, command), throw, debug, password)
|
|
11
|
+
except Exception as e:
|
|
12
|
+
if retry >= retries:
|
|
13
|
+
raise
|
|
14
|
+
retry += 1
|
|
15
|
+
time.sleep(sleep)
|
|
16
|
+
sleep = sleep * 2
|
|
17
|
+
print('retrying {0}'.format(retry))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_ssh(host, command, throw=True, debug=True, password='syncloud', retries=0, sleep=1, env_vars='', port=22):
|
|
21
|
+
ssh_command='{0} {1}'.format(env_vars, command)
|
|
22
|
+
retry = 0
|
|
23
|
+
while True:
|
|
24
|
+
try:
|
|
25
|
+
return _run_command('ssh -p {0} -o StrictHostKeyChecking=no root@{1} "{2}"'.format(port, host, ssh_command), throw, debug, password)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
if retry >= retries:
|
|
28
|
+
raise
|
|
29
|
+
retry += 1
|
|
30
|
+
time.sleep(sleep)
|
|
31
|
+
sleep = sleep * 2
|
|
32
|
+
print('retrying {0}'.format(retry))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ssh_command(password, command):
|
|
36
|
+
return 'sshpass -p {0} {1}'.format(password, command)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_command(command, throw, debug, password):
|
|
40
|
+
try:
|
|
41
|
+
print('ssh command: {0}'.format(command))
|
|
42
|
+
output = str(check_output(ssh_command(password, command), shell=True, stderr=STDOUT, encoding='UTF-8')).strip()
|
|
43
|
+
if debug:
|
|
44
|
+
print("ssh output: " + output)
|
|
45
|
+
print
|
|
46
|
+
return output
|
|
47
|
+
except CalledProcessError as e:
|
|
48
|
+
print("ssh error: " + str(e.output))
|
|
49
|
+
if throw:
|
|
50
|
+
raise
|
|
51
|
+
return str(e.output)
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import jsonpickle
|
|
4
|
+
import os
|
|
5
|
+
import datetime
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
jsonpickle.set_preferred_backend('json')
|
|
9
|
+
if sys.version_info.major < 3:
|
|
10
|
+
types = (bool, str, unicode, int, long, float, datetime.datetime)
|
|
11
|
+
else:
|
|
12
|
+
types = (bool, str, int, float, datetime.datetime)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pretty_print():
|
|
16
|
+
jsonpickle.set_encoder_options('json', indent=2)
|
|
17
|
+
|
|
18
|
+
def ugly_print():
|
|
19
|
+
jsonpickle.set_encoder_options('json', indent=None)
|
|
20
|
+
|
|
21
|
+
class List:
|
|
22
|
+
def __init__(self, item_type):
|
|
23
|
+
self.item_type = item_type
|
|
24
|
+
|
|
25
|
+
class Field:
|
|
26
|
+
def __init__(self, field_type=None, default=None):
|
|
27
|
+
self.field_type = field_type
|
|
28
|
+
self.default = default
|
|
29
|
+
|
|
30
|
+
class Struct(object):
|
|
31
|
+
def __init__(self, adict):
|
|
32
|
+
self.__dict__.update(adict)
|
|
33
|
+
for k, v in adict.items():
|
|
34
|
+
if isinstance(v, (dict, list)):
|
|
35
|
+
self.__dict__[k] = to_object(v)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_field_value(field, field_meta, structure):
|
|
39
|
+
if field in structure.keys():
|
|
40
|
+
value = structure[field]
|
|
41
|
+
if field_meta.field_type:
|
|
42
|
+
return to_object(value, field_meta.field_type)
|
|
43
|
+
return value
|
|
44
|
+
else:
|
|
45
|
+
if field_meta.default is not None:
|
|
46
|
+
return field_meta.default
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_object(structure, obj_type=None):
|
|
51
|
+
if obj_type:
|
|
52
|
+
if isinstance(obj_type, List):
|
|
53
|
+
return [to_object(item, obj_type.item_type) for item in structure]
|
|
54
|
+
else:
|
|
55
|
+
obj = obj_type()
|
|
56
|
+
for field, field_meta in inspect.getmembers(obj):
|
|
57
|
+
if isinstance(field_meta, Field):
|
|
58
|
+
value = get_field_value(field, field_meta, structure)
|
|
59
|
+
setattr(obj, field, value)
|
|
60
|
+
return obj
|
|
61
|
+
else:
|
|
62
|
+
if structure is None or isinstance(structure, types):
|
|
63
|
+
return structure
|
|
64
|
+
if isinstance(structure, list):
|
|
65
|
+
return [to_object(item) for item in structure]
|
|
66
|
+
return Struct(structure)
|
|
67
|
+
|
|
68
|
+
def limit(dictionary, keys):
|
|
69
|
+
filtered = [key for key in dictionary.keys() if key not in keys]
|
|
70
|
+
for key in filtered:
|
|
71
|
+
dictionary.pop(key, None)
|
|
72
|
+
|
|
73
|
+
def to_dict(value):
|
|
74
|
+
if isinstance(value, list):
|
|
75
|
+
return [to_dict(item) for item in value]
|
|
76
|
+
if isinstance(value, dict):
|
|
77
|
+
cloned = value.copy()
|
|
78
|
+
for key, value in cloned.items():
|
|
79
|
+
cloned[key] = to_dict(value)
|
|
80
|
+
return cloned
|
|
81
|
+
if value is None or isinstance(value, types):
|
|
82
|
+
return value
|
|
83
|
+
result = value.__dict__.copy()
|
|
84
|
+
if hasattr(value, '__public__'):
|
|
85
|
+
limit(result, value.__public__)
|
|
86
|
+
for member, mvalue in result.items():
|
|
87
|
+
result[member] = to_dict(mvalue)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
def from_json(json_text, obj_type=None):
|
|
91
|
+
return to_object(jsonpickle.decode(json_text), obj_type)
|
|
92
|
+
|
|
93
|
+
def to_json(value):
|
|
94
|
+
return jsonpickle.encode(value, unpicklable=False)
|
|
95
|
+
|
|
96
|
+
def reformat(json_text):
|
|
97
|
+
return to_json(from_json(json_text))
|
|
98
|
+
|
|
99
|
+
def read_json(filename, obj_type=None):
|
|
100
|
+
if not os.path.isfile(filename):
|
|
101
|
+
return None
|
|
102
|
+
with open(filename, 'r') as f:
|
|
103
|
+
json = f.read()
|
|
104
|
+
if json.strip(' \t\n\r') == '':
|
|
105
|
+
return None
|
|
106
|
+
return from_json(json, obj_type)
|
|
107
|
+
|
|
108
|
+
def write_json(filename, obj):
|
|
109
|
+
if not obj:
|
|
110
|
+
json = ''
|
|
111
|
+
else:
|
|
112
|
+
json = to_json(obj)
|
|
113
|
+
with open(filename, 'w') as file:
|
|
114
|
+
file.write(json)
|
syncloudlib/linux.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from subprocess import check_output
|
|
2
|
+
import pwd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def useradd(user, home_folder=None, shell='/bin/false'):
|
|
6
|
+
try:
|
|
7
|
+
pwd.getpwnam(user)
|
|
8
|
+
return 'user {0} exists'.format(user)
|
|
9
|
+
except KeyError:
|
|
10
|
+
options = '-r -s {0}'.format(shell)
|
|
11
|
+
if home_folder:
|
|
12
|
+
home_folder_options = '-m -d {0}'.format(home_folder)
|
|
13
|
+
options = home_folder_options + ' ' + options
|
|
14
|
+
command_line = '/usr/sbin/useradd {0} {1}'.format(options, user)
|
|
15
|
+
return check_output(command_line, shell=True).decode()
|
syncloudlib/logger.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.handlers
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
factory_instance = None
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WhitespaceRemovingFormatter(logging.Formatter):
|
|
9
|
+
def format(self, record):
|
|
10
|
+
record.msg = clean(record.msg)
|
|
11
|
+
return super(WhitespaceRemovingFormatter, self).format(record)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def clean(message):
|
|
15
|
+
return str(message).strip()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggerFactory:
|
|
19
|
+
def __init__(self, level, console, filename, line_format):
|
|
20
|
+
self.level = level
|
|
21
|
+
self.console = console
|
|
22
|
+
self.filename = filename
|
|
23
|
+
self.formatter = WhitespaceRemovingFormatter(line_format)
|
|
24
|
+
if filename:
|
|
25
|
+
self.file_handler = logging.handlers.RotatingFileHandler(self.filename, maxBytes=1000000, backupCount=5)
|
|
26
|
+
self.file_handler.setFormatter(self.formatter)
|
|
27
|
+
if console:
|
|
28
|
+
self.console_handler = logging.StreamHandler(sys.stdout)
|
|
29
|
+
self.console_handler.setFormatter(self.formatter)
|
|
30
|
+
|
|
31
|
+
def get_logger(self, name):
|
|
32
|
+
logger = logging.getLogger(name)
|
|
33
|
+
logger.setLevel(self.level)
|
|
34
|
+
if self.filename:
|
|
35
|
+
if self.file_handler not in logger.handlers:
|
|
36
|
+
logger.addHandler(self.file_handler)
|
|
37
|
+
if self.console:
|
|
38
|
+
if self.console_handler not in logger.handlers:
|
|
39
|
+
logger.addHandler(self.console_handler)
|
|
40
|
+
return logger
|
|
41
|
+
|
|
42
|
+
default_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def init(level=logging.INFO, console=False, filename=None, line_format=default_format):
|
|
46
|
+
global factory_instance
|
|
47
|
+
factory_instance = LoggerFactory(level, console, filename, line_format)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_logger(name):
|
|
51
|
+
global factory_instance
|
|
52
|
+
if not factory_instance:
|
|
53
|
+
raise Exception('Logging is not initialized')
|
|
54
|
+
return factory_instance.get_logger(name)
|