syneto_api 0.1.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: syneto-api
3
+ Version: 0.1.0
4
+ Summary: Syneto Client API library
5
+ License: Proprietary
6
+ Author: Your Name
7
+ Author-email: you@example.com
8
+ Requires-Python: >=3.6,<4.0
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.6
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Requires-Dist: python-dotenv (>=0.16.0,<0.17.0)
15
+ Requires-Dist: requests[secure] (>=2.25.1,<3.0.0)
@@ -0,0 +1,19 @@
1
+ [tool.poetry]
2
+ name = "syneto_api"
3
+ version = "0.1.0"
4
+ description = "Syneto Client API library"
5
+ authors = ["Your Name <you@example.com>"]
6
+ license = "Proprietary"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.6"
10
+ python-dotenv = "^0.16.0"
11
+ requests = {extras = ["secure"], version = "^2.25.1"}
12
+
13
+ [tool.poetry.dev-dependencies]
14
+ pytest = "^6.2.2"
15
+ ipdb = "^0.13.7"
16
+
17
+ [build-system]
18
+ requires = ["poetry>=0.12"]
19
+ build-backend = "poetry.masonry.api"
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ from setuptools import setup
3
+
4
+ package_dir = \
5
+ {'': 'src'}
6
+
7
+ packages = \
8
+ ['syneto_api']
9
+
10
+ package_data = \
11
+ {'': ['*']}
12
+
13
+ install_requires = \
14
+ ['python-dotenv>=0.16.0,<0.17.0', 'requests[secure]>=2.25.1,<3.0.0']
15
+
16
+ setup_kwargs = {
17
+ 'name': 'syneto-api',
18
+ 'version': '0.1.0',
19
+ 'description': 'Syneto Client API library',
20
+ 'long_description': None,
21
+ 'author': 'Your Name',
22
+ 'author_email': 'you@example.com',
23
+ 'maintainer': None,
24
+ 'maintainer_email': None,
25
+ 'url': None,
26
+ 'package_dir': package_dir,
27
+ 'packages': packages,
28
+ 'package_data': package_data,
29
+ 'install_requires': install_requires,
30
+ 'python_requires': '>=3.6,<4.0',
31
+ }
32
+
33
+
34
+ setup(**setup_kwargs)
@@ -0,0 +1,4 @@
1
+ from .auth_api import Authentication
2
+ from .virt_api import Virtualization
3
+ from .storage_api import Storage
4
+ from .protection_api import Protection
@@ -0,0 +1,177 @@
1
+ import os
2
+ import requests
3
+ import urllib.parse
4
+ import logging
5
+
6
+ from .settings import get_env_flag
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def make_url(url_base, *path_args, query_args: dict = None, **path_kwargs):
12
+ path_args = [urllib.parse.quote(v, safe="") for v in path_args]
13
+ path_kwargs = {k: urllib.parse.quote(v, safe="") for k, v in path_kwargs.items()}
14
+ path = url_base.format(*path_args, **path_kwargs)
15
+
16
+ if query_args is None or len(query_args) == 0:
17
+ return path
18
+ query_string = urllib.parse.urlencode(
19
+ query_args, doseq=True, quote_via=urllib.parse.quote
20
+ )
21
+ return f"{path}?{query_string}"
22
+
23
+
24
+ class APIException(Exception):
25
+ def __init__(
26
+ self, url: str, status_code: int = 0, request_body: dict = None, response=None
27
+ ):
28
+ self.url = url
29
+ self.status_code = status_code
30
+ self.request_body = request_body
31
+ self.response = response
32
+ message = f"API Error: {self.url}"
33
+ if self.status_code and self.response is not None:
34
+ message = (
35
+ f"API {self.url} returned HTTP {self.status_code}: {self.response.text}"
36
+ )
37
+ super().__init__(message)
38
+
39
+
40
+ class APIClientBase:
41
+ SUCCESS_CODES = {
42
+ "GET": [200],
43
+ "POST": [200, 201, 202],
44
+ "PUT": [200, 202],
45
+ "PATCH": [200, 202],
46
+ "DELETE": [200, 204],
47
+ }
48
+
49
+ def __init__(self, url_base=None, default_protocol="http", insecure_ssl=None):
50
+ self.url_base = url_base
51
+ protocols = ["http://", "https://"]
52
+ if not any(self.url_base.lower().startswith(p) for p in protocols):
53
+ self.url_base = f"{default_protocol}://{self.url_base}"
54
+
55
+ self.url_base = self.url_base.rstrip("/")
56
+
57
+ self.headers = {}
58
+ self._verify_ssl_cert = True
59
+ if insecure_ssl is not None:
60
+ self.set_insecure_ssl(insecure_ssl)
61
+ elif get_env_flag("ALLOW_INSECURE_SSL"):
62
+ self.set_insecure_ssl(True)
63
+
64
+ def _request(
65
+ self,
66
+ method: str,
67
+ relative_url: str,
68
+ *args,
69
+ query_args: dict = None,
70
+ body: dict = None,
71
+ success_codes: list = None,
72
+ **kwargs,
73
+ ):
74
+ relative_url = relative_url.lstrip("/")
75
+ url = make_url(
76
+ self.url_base + "/" + relative_url, *args, query_args=query_args, **kwargs
77
+ )
78
+ logger.debug(f"Calling {method} {url}")
79
+
80
+ if not success_codes:
81
+ success_codes = APIClientBase.SUCCESS_CODES[method]
82
+
83
+ method_func = self._get_method_func(method)
84
+ method_args = {"verify": self._verify_ssl_cert}
85
+ if self.headers is not None and len(self.headers) > 0:
86
+ method_args["headers"] = self.headers
87
+
88
+ if body is not None:
89
+ method_args["json"] = body
90
+
91
+ try:
92
+ response = method_func(url, timeout=20, **method_args)
93
+ if response.status_code in success_codes:
94
+ logger.debug("Received {response.status_code} from {method} {url}")
95
+ return response.json()
96
+
97
+ logger.info("Received {response.status_code} from {method} {url}")
98
+ raise APIException(
99
+ url=url,
100
+ status_code=response.status_code,
101
+ request_body=body,
102
+ response=response,
103
+ )
104
+
105
+ except APIException as e:
106
+ raise
107
+
108
+ except TypeError:
109
+ raise
110
+
111
+ except Exception as e:
112
+ logger.warning(f"Failed {method} request to {url}: {str(e)}")
113
+ raise APIException(url=url, request_body=body,) from e
114
+
115
+ def _get_method_func(self, method):
116
+ requests_methods = {
117
+ "GET": requests.get,
118
+ "POST": requests.post,
119
+ "PUT": requests.put,
120
+ "PATCH": requests.patch,
121
+ "DELETE": requests.delete,
122
+ }
123
+ return requests_methods[method]
124
+
125
+ def get_request(
126
+ self,
127
+ relative_url: str,
128
+ *url_path_args,
129
+ query_args: dict = None,
130
+ success_codes: list = None,
131
+ **url_path_kwargs,
132
+ ):
133
+ return self._request(
134
+ "GET",
135
+ relative_url,
136
+ *url_path_args,
137
+ query_args=query_args,
138
+ success_codes=success_codes,
139
+ **url_path_kwargs,
140
+ )
141
+
142
+ def delete_request(self, *args, **kwargs):
143
+ # Same args as get_request
144
+ return self._request("DELETE", *args, **kwargs)
145
+
146
+ def post_request(
147
+ self,
148
+ relative_url: str,
149
+ *url_path_args,
150
+ query_args: dict = None,
151
+ body: dict = None,
152
+ success_codes: list = None,
153
+ **url_path_kwargs,
154
+ ):
155
+ return self._request(
156
+ "POST",
157
+ relative_url,
158
+ *url_path_args,
159
+ query_args=query_args,
160
+ body=body,
161
+ success_codes=success_codes,
162
+ **url_path_kwargs,
163
+ )
164
+
165
+ def put_request(self, *args, **kwargs):
166
+ # Same args as post_request
167
+ return self._request("PUT", *args, **kwargs)
168
+
169
+ def patch_request(self, *args, **kwargs):
170
+ # Same args as post_request
171
+ return self._request("PATCH", *args, **kwargs)
172
+
173
+ def set_insecure_ssl(self, insecure=True):
174
+ self._verify_ssl_cert = not insecure
175
+
176
+ def set_auth_jwt(self, jwt):
177
+ self.headers["Authorization"] = f"Bearer {jwt}"
@@ -0,0 +1,11 @@
1
+ import os
2
+ from .api_client import APIClientBase
3
+
4
+
5
+ class Authentication(APIClientBase):
6
+ def __init__(self, url_base=None, **kwargs):
7
+ super().__init__(url_base or os.environ.get("AUTH_SERVICE", ""), **kwargs)
8
+
9
+ def login(self, username: str, password: str):
10
+ body = {"username": username, "password": password}
11
+ return self.post_request("/login", body=body)
@@ -0,0 +1,37 @@
1
+ import os
2
+ from .api_client import APIClientBase
3
+
4
+
5
+ class Protection(APIClientBase):
6
+ def __init__(self, url_base=None, **kwargs):
7
+ super().__init__(url_base or os.environ.get("PROTECTION_SERVICE", ""), **kwargs)
8
+
9
+ def get_policies(self):
10
+ return self.get_request("/policies/")
11
+
12
+ def get_protected_vms(self):
13
+ return self.get_request("/vms/")
14
+
15
+ def get_jobs(self):
16
+ return self.get_request("/jobs/")
17
+
18
+ def get_job(self, id):
19
+ return self.get_request("/jobs/{}", id)
20
+
21
+ def patch_job(self, id, payload):
22
+ return self.patch_request("/jobs/", query_args={"id": id}, body=payload)
23
+
24
+ def remove_job(self, id):
25
+ return self.delete_request("/jobs/", query_args={"id": id})
26
+
27
+ def create_external_vm_protection_job(self, config):
28
+ body = {"config": config}
29
+ return self.post_request("/jobs/protect-external-vm", body=body)
30
+
31
+ def create_local_vm_protection_job(self, config):
32
+ body = {"config": config}
33
+ return self.post_request("/jobs/protect-local-vm", body=body)
34
+
35
+ def create_local_vm_replication_job(self, config):
36
+ body = {"config": config}
37
+ return self.post_request("/jobs/replicate-local-vm", body=body)
@@ -0,0 +1,39 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ bool_values = {"true": True, "yes": True, "false": False, "no": False}
7
+
8
+
9
+ def get_env_flag(env_key: str) -> bool:
10
+ """
11
+ Returns the parsed value of the given env. var (assumed of boolean type).
12
+ The values "True" and "Yes" (case-insensitive) are parsed as True while
13
+ everything else (including non-existent env. var) parses as False.
14
+ """
15
+ env_value = os.environ.get(env_key, "")
16
+ return bool_values.get(env_value.lower()) == True
17
+
18
+
19
+ def get_env_int(env_key, default=0) -> int:
20
+ """
21
+ Returns the parsed value of the given env. var (assumed of integer type).
22
+ If the env. var does not exist, the given default is returned. If the env. var value
23
+ fails to parse, a ValueError is raised.
24
+ """
25
+ env_value = os.environ.get(env_key, f"{default}")
26
+ return int(env_value)
27
+
28
+
29
+ def get_env_string(env_key, default="") -> str:
30
+ """
31
+ Returns the associated os.environ value for the specified value as a string.
32
+ """
33
+ env_value = os.environ.get(env_key, f"{default}")
34
+ return env_value
35
+
36
+
37
+ def get_root_path():
38
+ root_path_env = os.environ.get("ROOT_PATH", "") or ""
39
+ return "/" + root_path_env.strip("/")
@@ -0,0 +1,130 @@
1
+ import os
2
+ from .api_client import APIClientBase
3
+
4
+
5
+ class Storage(APIClientBase):
6
+ def __init__(self, url_base=None, **kwargs):
7
+ super().__init__(url_base or os.environ.get("STORAGE_SERVICE", ""), **kwargs)
8
+
9
+ def get_pools(self):
10
+ return self.get_request("/pools")
11
+
12
+ def get_importable_pools(self):
13
+ return self.get_request("/pools/importable")
14
+
15
+ def get_pool(self, pool_name: str):
16
+ return self.get_request("/pools/{pool_name}", pool_name=pool_name)
17
+
18
+ def clear_pool(self, pool_name: str):
19
+ return self.put_request("/pools/{pool_name}/clear", pool_name=pool_name)
20
+
21
+ def upgrade_pool(self, pool_name: str):
22
+ return self.put_request("/pools/{pool_name}/upgrade", pool_name=pool_name)
23
+
24
+ def export_pool(self, pool_name: str):
25
+ return self.put_request("/pools/{pool_name}/export", pool_name=pool_name)
26
+
27
+ def get_pool_periodic_scrub(self, pool_name: str):
28
+ return self.get_request(
29
+ "/pools/{pool_name}/scrub/interval", pool_name=pool_name
30
+ )
31
+
32
+ def set_pool_periodic_scrub(self, pool_name: str, interval: str):
33
+ return self.post_request(
34
+ "/pools/{pool_name}/scrub/interval",
35
+ pool_name=pool_name,
36
+ query_args={"interval": interval},
37
+ )
38
+
39
+ def delete_pool_periodic_scrub(self, pool_name: str):
40
+ return self.delete_request(
41
+ "/pools/{pool_name}/scrub/interval", pool_name=pool_name
42
+ )
43
+
44
+ def start_pool_scrub(self, pool_name: str):
45
+ return self.put_request("/pools/{pool_name}/scrub/start", pool_name=pool_name)
46
+
47
+ def stop_pool_scrub(self, pool_name: str):
48
+ return self.put_request("/pools/{pool_name}/scrub/stop", pool_name=pool_name)
49
+
50
+ def get_volumes(self):
51
+ return self.get_request("/volumes")
52
+
53
+ def create_volume(
54
+ self, pool_name: str, ips: list, volume_type: str = "NFS", quota: str = "none"
55
+ ):
56
+ hosts = [{"ipNetwork": ip, "accessType": "rw"} for ip in ips]
57
+ body = {
58
+ "type": volume_type,
59
+ "pool": pool_name,
60
+ "nfsShareConfig": {"rootAccess": "true", "hosts": hosts},
61
+ "quota": quota,
62
+ }
63
+ return self.post_request("/volumes", body=body,)
64
+
65
+ def get_volume(self, volume_id: str):
66
+ return self.get_request("/volumes/{volume_id}", volume_id=volume_id,)
67
+
68
+ def delete_volume(self, volume_id: str):
69
+ return self.delete_request(
70
+ "/volumes/{volume_id}", query_args={"volume_id": volume_id},
71
+ )
72
+
73
+ def promote_volume(self, volume_id: str):
74
+ return self.put_request("/volumes/{volume_id}/promote", volume_id=volume_id)
75
+
76
+ def get_volume_meta(self, volume_id: str, key: str):
77
+ return self.put_request(
78
+ "/volumes/{volume_id}/meta", volume_id=volume_id, query_args={"key": key}
79
+ )
80
+
81
+ def set_volume_meta(self, volume_id: str, key: str, value: dict):
82
+ body = {"key": key, "value": value}
83
+ return self.put_request(
84
+ "/volumes/{volume_id}/meta", volume_id=volume_id, body=body,
85
+ )
86
+
87
+ def get_volume_snapshots(self, volume_id: str):
88
+ return self.get_request("/volumes/{volume_id}/snapshots", volume_id=volume_id,)
89
+
90
+ def create_volume_snapshot(self, volume_id: str, snapshot_name: str):
91
+ return self.post_request(
92
+ "/volumes/{volume_id}/snapshots",
93
+ volume_id=volume_id,
94
+ query_args={"snapshot_name": snapshot_name},
95
+ )
96
+
97
+ def get_volume_snapshot(self, volume_id: str, snapshot_name: str):
98
+ return self.get_request(
99
+ "/volumes/{volume_id}/snapshots/{snapshot_name}",
100
+ volume_id=volume_id,
101
+ snapshot_name=snapshot_name,
102
+ )
103
+
104
+ def remove_volume_snapshot(self, volume_id: str, snapshot_name: str):
105
+ return self.delete_request(
106
+ "/volumes/{volume_id}/snapshots",
107
+ volume_id=volume_id,
108
+ query_args={"snapshot_name": snapshot_name},
109
+ )
110
+
111
+ def rollback_volume_snapshot(self, volume_id: str, snapshot_name: str):
112
+ return self.put_request(
113
+ "/volumes/{volume_id}/snapshots/{snapshot_name}/rollback",
114
+ volume_id=volume_id,
115
+ snapshot_name=snapshot_name,
116
+ )
117
+
118
+ def get_volume_snapshot_clones(self, volume_id: str, snapshot_name: str):
119
+ return self.get_request(
120
+ "/volumes/{volume_id}/snapshots/{snapshot_name}/clones",
121
+ volume_id=volume_id,
122
+ snapshot_name=snapshot_name,
123
+ )
124
+
125
+ def create_volume_snapshot_clone(self, volume_id: str, snapshot_name: str):
126
+ return self.post_request(
127
+ "/volumes/{volume_id}/snapshots/{snapshot_name}/clones",
128
+ volume_id=volume_id,
129
+ snapshot_name=snapshot_name,
130
+ )
@@ -0,0 +1,21 @@
1
+ import os
2
+ from .api_client import APIClientBase
3
+
4
+
5
+ class Virtualization(APIClientBase):
6
+ def __init__(self, url_base=None, **kwargs):
7
+ super().__init__(
8
+ url_base or os.environ.get("VIRTUALIZATION_SERVICE", ""), **kwargs
9
+ )
10
+
11
+ def get_hypervisors(self):
12
+ return self.get_request("/hypervisors/")
13
+
14
+ def get_vms(self):
15
+ return self.get_request("/hypervisors/vms")
16
+
17
+ def get_hosts(self):
18
+ return self.get_request("/hypervisors/hosts")
19
+
20
+ def get_datastores(self):
21
+ return self.get_request("/hypervisors/datastores")