drove-cli 0.0.1__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.
- drove.py +53 -0
- drove_cli-0.0.1.dist-info/METADATA +787 -0
- drove_cli-0.0.1.dist-info/RECORD +14 -0
- drove_cli-0.0.1.dist-info/WHEEL +4 -0
- drove_cli-0.0.1.dist-info/entry_points.txt +3 -0
- drovecli.py +37 -0
- droveclient.py +192 -0
- droveutils.py +91 -0
- plugins/__init__.py +47 -0
- plugins/appinstances.py +169 -0
- plugins/applications.py +233 -0
- plugins/cluster.py +116 -0
- plugins/executors.py +129 -0
- plugins/tasks.py +155 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
drove.py,sha256=8RzbZU_hpHdxcJy9oDeYKh5bNmD54b7TKB1DJ9QtN18,1807
|
|
2
|
+
drovecli.py,sha256=u8oJc15Bxl9flhvDjJMJUyNWKz1wo35XR4ywMhT1gWs,1168
|
|
3
|
+
droveclient.py,sha256=0M0dl_H4m90pQFCgrF-Nz6JJfu9ycN7YQY0E3RaZdkU,8561
|
|
4
|
+
droveutils.py,sha256=-Yook6cYnStFDox_9CFFV9Ox1ywCQCzKTyMx9cg6jKc,4026
|
|
5
|
+
plugins/__init__.py,sha256=jp5G2AnrSmrENhsDe_6iBLUHbKA9DYDO6wYIOujv4YM,1327
|
|
6
|
+
plugins/appinstances.py,sha256=Wtg3suTiaIWTrQHK0JUHfSON2rdVgRv65Zab_2uKjhg,9188
|
|
7
|
+
plugins/applications.py,sha256=u8lgsiFpCol6v84PMghleo_JlaYpZuSRFkoeUZx41Ho,11752
|
|
8
|
+
plugins/cluster.py,sha256=TG2GthGtShMdsJrMJD7NMDnqpRoalzGz9wNFRDbXPXY,5635
|
|
9
|
+
plugins/executors.py,sha256=AnWfX6FxZd5-f4M7Wzp5RXdId0DUf0USnmH8XicChno,7005
|
|
10
|
+
plugins/tasks.py,sha256=q-aL2B2NePTmHDW83mKSYn4XiWYkujt1dTqjG5sHoCs,7966
|
|
11
|
+
drove_cli-0.0.1.dist-info/METADATA,sha256=1zlH82KH0yYBiF4igoSExNHYeWdwB1p-xrAet1GJGgw,20425
|
|
12
|
+
drove_cli-0.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
13
|
+
drove_cli-0.0.1.dist-info/entry_points.txt,sha256=PBYfLCyK6IJMnqgttT0ATxjl7h2AgLDK4zF7gSNoFGM,35
|
|
14
|
+
drove_cli-0.0.1.dist-info/RECORD,,
|
drovecli.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import droveclient
|
|
3
|
+
from plugins import DrovePlugin
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DroveCli:
|
|
9
|
+
def __init__(self, parser: argparse.ArgumentParser):
|
|
10
|
+
self.parser = parser
|
|
11
|
+
self.drove_client = droveclient.DroveClient()
|
|
12
|
+
self.plugins: list = []
|
|
13
|
+
self.debug = False
|
|
14
|
+
subparsers = parser.add_subparsers(help="Available plugins")
|
|
15
|
+
for plugin_class in DrovePlugin.plugins:
|
|
16
|
+
plugin = plugin_class()
|
|
17
|
+
# print("Loading plugin: " + str(plugin))
|
|
18
|
+
plugin.populate_options(drove_client=self.drove_client, subparser=subparsers)
|
|
19
|
+
self.plugins.append(plugin)
|
|
20
|
+
parser.set_defaults(func=self.show_help)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(self):
|
|
24
|
+
args = self.parser.parse_args()
|
|
25
|
+
self.debug = args.debug
|
|
26
|
+
drove_client = droveclient.build_drove_client(self.drove_client, args=args)
|
|
27
|
+
if drove_client is None:
|
|
28
|
+
return None
|
|
29
|
+
self.drove_client = drove_client
|
|
30
|
+
|
|
31
|
+
# Load plugins
|
|
32
|
+
|
|
33
|
+
args.func(args)
|
|
34
|
+
|
|
35
|
+
def show_help(self, options: SimpleNamespace) -> None:
|
|
36
|
+
self.parser.print_help()
|
|
37
|
+
exit(-1)
|
droveclient.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import requests
|
|
5
|
+
import urllib3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from requests.adapters import HTTPAdapter, Retry
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
class TokenAuth(requests.auth.AuthBase):
|
|
11
|
+
def __init__(self, token: str):
|
|
12
|
+
self.token = token
|
|
13
|
+
super().__init__()
|
|
14
|
+
|
|
15
|
+
def __call__(self, r):
|
|
16
|
+
r.headers.update({"Authorization": self.token})
|
|
17
|
+
return r
|
|
18
|
+
|
|
19
|
+
class DroveException(Exception):
|
|
20
|
+
"""Exception raised while calling drove endpoint"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, status_code: int, message: str, raw: str = None, api_response: dict = None):
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.raw = raw
|
|
25
|
+
self.api_response = api_response
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
|
|
28
|
+
class DroveClient:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.endpoint: str = None
|
|
31
|
+
self.auth_header: str = None
|
|
32
|
+
self.username = None
|
|
33
|
+
self.password = None
|
|
34
|
+
self.insecure: bool = False
|
|
35
|
+
self.session = requests.session()
|
|
36
|
+
retries = Retry(connect=5,
|
|
37
|
+
read=5,
|
|
38
|
+
backoff_factor=0.1)
|
|
39
|
+
|
|
40
|
+
self.session.mount('https://', HTTPAdapter(max_retries=retries))
|
|
41
|
+
self.session.mount('http://', HTTPAdapter(max_retries=retries))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def start(self,
|
|
45
|
+
endpoint: str = None,
|
|
46
|
+
auth_header: str = None,
|
|
47
|
+
username: str = None,
|
|
48
|
+
password: str = None,
|
|
49
|
+
insecure: bool = False):
|
|
50
|
+
self.endpoint = endpoint
|
|
51
|
+
self.auth_header = auth_header
|
|
52
|
+
self.insecure = insecure
|
|
53
|
+
self.session.verify = not insecure
|
|
54
|
+
if insecure:
|
|
55
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
56
|
+
if username != None and password != None:
|
|
57
|
+
self.session.auth = requests.auth.HTTPBasicAuth(username, password)
|
|
58
|
+
elif auth_header:
|
|
59
|
+
self.session.auth = TokenAuth(auth_header)
|
|
60
|
+
|
|
61
|
+
self.get("/apis/v1/ping")
|
|
62
|
+
# print("Connection validated for endpoint: " + self.endpoint)
|
|
63
|
+
|
|
64
|
+
def app_instances(self, app_id: str, healthy_only: bool = True):
|
|
65
|
+
data = self.get("/apis/v1/applications/{app_id}/instances".format(app_id=app_id))
|
|
66
|
+
if healthy_only:
|
|
67
|
+
instances = [instance["instanceId"] for instance in data if instance["state"] == "HEALTHY"]
|
|
68
|
+
else:
|
|
69
|
+
instances = [instance["instanceId"] for instance in data]
|
|
70
|
+
return set(instances)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get(self, path: str, expected_status = 200) -> dict:
|
|
74
|
+
try:
|
|
75
|
+
response = self.session.get(self.endpoint + path)
|
|
76
|
+
except requests.ConnectionError as e:
|
|
77
|
+
raise DroveException(-1, "Error connecting to endpoint " + self.endpoint, raw={})
|
|
78
|
+
return handle_drove_response(response, expected_status)
|
|
79
|
+
|
|
80
|
+
def get_raw(self, path: str, expected_status: int = 200) -> dict:
|
|
81
|
+
try:
|
|
82
|
+
response = self.session.get(self.endpoint + path)
|
|
83
|
+
except requests.ConnectionError as e:
|
|
84
|
+
raise DroveException(-1, "Error connecting to endpoint " + self.endpoint, raw={})
|
|
85
|
+
|
|
86
|
+
status_code = response.status_code
|
|
87
|
+
text = response.text
|
|
88
|
+
if status_code != expected_status:
|
|
89
|
+
raise DroveException(status_code, "Drove call failed with status: " + str(status_code), raw=text)
|
|
90
|
+
try:
|
|
91
|
+
return json.loads(text)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise DroveException(status_code, str(e))
|
|
94
|
+
|
|
95
|
+
def get_to_file(self, path: str, filename: str, expected_status: int = 200) -> int:
|
|
96
|
+
size = 0
|
|
97
|
+
try:
|
|
98
|
+
with self.session.get(self.endpoint + path, stream=True) as r:
|
|
99
|
+
if r.status_code != expected_status:
|
|
100
|
+
raise DroveException(r.status_code, "Drove call failed with status: " + str(r.status_code))
|
|
101
|
+
with open(filename, 'wb') as f:
|
|
102
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
103
|
+
# If you have chunk encoded response uncomment if
|
|
104
|
+
# and set chunk_size parameter to None.
|
|
105
|
+
#if chunk:
|
|
106
|
+
f.write(chunk)
|
|
107
|
+
size = size + len(chunk)
|
|
108
|
+
return size
|
|
109
|
+
except DroveException:
|
|
110
|
+
raise
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise DroveException(-1, str(e))
|
|
113
|
+
|
|
114
|
+
def post(self, path: str, body: dict, parse=True, expected_status = 200) -> dict:
|
|
115
|
+
try:
|
|
116
|
+
response = self.session.post(self.endpoint + path, json=body)
|
|
117
|
+
except requests.ConnectionError as e:
|
|
118
|
+
raise DroveException(-1, "Error connecting to endpoint " + self.endpoint, raw={})
|
|
119
|
+
return handle_drove_response(response, expected_status)
|
|
120
|
+
|
|
121
|
+
def handle_drove_response(response: requests.Response, expected_status: int):
|
|
122
|
+
status_code = response.status_code
|
|
123
|
+
text = response.text
|
|
124
|
+
api_response = None
|
|
125
|
+
try:
|
|
126
|
+
if text != None and response.json() != None:
|
|
127
|
+
api_response = response.json()
|
|
128
|
+
except json.decoder.JSONDecodeError:
|
|
129
|
+
raise DroveException(status_code, text)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
raise DroveException(status_code, str(e))
|
|
132
|
+
if status_code != expected_status:
|
|
133
|
+
raise DroveException(status_code,
|
|
134
|
+
"Drove call failed with status code: {code}, error: {message}".format(code=status_code, message=text),
|
|
135
|
+
api_response=api_response)
|
|
136
|
+
if api_response == None:
|
|
137
|
+
raise DroveException(status_code, "Drove call failed with status code: {code}".format(code=status_code))
|
|
138
|
+
|
|
139
|
+
if "status" in api_response:
|
|
140
|
+
if api_response["status"] != "SUCCESS":
|
|
141
|
+
raise DroveException(status_code, message = api_response.get("message", ""), raw=text, api_response=api_response)
|
|
142
|
+
else:
|
|
143
|
+
raise DroveException(status_code, text)
|
|
144
|
+
return api_response["data"] if "data" in api_response else api_response
|
|
145
|
+
|
|
146
|
+
def build_drove_client(drove_client: DroveClient, args: SimpleNamespace):
|
|
147
|
+
endpoint = args.endpoint
|
|
148
|
+
auth_header = args.auth_header
|
|
149
|
+
insecure = args.insecure
|
|
150
|
+
username = args.username
|
|
151
|
+
password = args.password
|
|
152
|
+
|
|
153
|
+
if endpoint is None:
|
|
154
|
+
# If cmdl options are not passed, see if config file is passed
|
|
155
|
+
# If config file path is not passed, see if .drove exists in home and use that
|
|
156
|
+
config_file = args.file if args.file is not None else str(Path.home()) + "/.drove"
|
|
157
|
+
# Try to parse config if it exists and is readable
|
|
158
|
+
if os.path.isfile(config_file) and os.access(config_file, os.R_OK):
|
|
159
|
+
config_parser = configparser.ConfigParser()
|
|
160
|
+
try:
|
|
161
|
+
with open(config_file) as stream:
|
|
162
|
+
config_parser.read_string(stream.read())
|
|
163
|
+
drove_config = config_parser['DEFAULT']
|
|
164
|
+
if args.cluster is not None:
|
|
165
|
+
if args.cluster in config_parser:
|
|
166
|
+
drove_config = config_parser[args.cluster]
|
|
167
|
+
else:
|
|
168
|
+
print("error: No cluster definition found for {cluster} in config {config_file}".format(config_file=config_file, cluster=args.cluster))
|
|
169
|
+
return None
|
|
170
|
+
endpoint = drove_config.get("endpoint")
|
|
171
|
+
username = drove_config.get("username")
|
|
172
|
+
password = drove_config.get("password")
|
|
173
|
+
auth_header = drove_config.get("auth_header", None)
|
|
174
|
+
insecure = drove_config.get("insecure", False)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
#Looks like some random file was passed. Bail out
|
|
177
|
+
print("Error parsing config file " + config_file + ": " + str(e))
|
|
178
|
+
return None
|
|
179
|
+
else:
|
|
180
|
+
print("Error: Config file {config_file} is not present or readable".format(config_file=config_file))
|
|
181
|
+
return None
|
|
182
|
+
# At least endpoint is needed
|
|
183
|
+
if endpoint == None:
|
|
184
|
+
raise Exception("Error: provide config file or required command line params for drove connectivity\n")
|
|
185
|
+
endpoint = endpoint[:-1] if endpoint.endswith('/') else endpoint
|
|
186
|
+
if args.debug:
|
|
187
|
+
print('Endpoint: {endpoint} Username: {has_username} Password: {has_password} AuthHeader: {has_auth_header} Insecure: {insecure}'
|
|
188
|
+
.format(endpoint=endpoint, has_username=username is not None, has_password=password is not None,
|
|
189
|
+
has_auth_header=auth_header is not None, insecure=insecure))
|
|
190
|
+
drove_client.start(endpoint, auth_header, username, password, insecure)
|
|
191
|
+
return drove_client
|
|
192
|
+
|
droveutils.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import droveclient
|
|
3
|
+
import json
|
|
4
|
+
import tabulate
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
def print_dict(data: dict, level: int = 0):
|
|
8
|
+
for key, value in data.items():
|
|
9
|
+
print(level * 4 * " ", end='')
|
|
10
|
+
if type(value) is dict and not len(dict(value)) == 0:
|
|
11
|
+
print(f"{key: <30}")
|
|
12
|
+
print_dict(value, level + 1)
|
|
13
|
+
elif type(value) is list and all(isinstance(n, dict) for n in value):
|
|
14
|
+
print(f"{key: <30}")
|
|
15
|
+
for item in value:
|
|
16
|
+
print_dict(item, level + 1)
|
|
17
|
+
print()
|
|
18
|
+
else:
|
|
19
|
+
print(f"{key: <30}{value}")
|
|
20
|
+
|
|
21
|
+
def print_json(data: dict):
|
|
22
|
+
print(json.dumps(data, indent = 4))
|
|
23
|
+
|
|
24
|
+
def print_table(headers: list, data: list):
|
|
25
|
+
print(tabulate.tabulate(data, headers=headers))
|
|
26
|
+
|
|
27
|
+
def print_dict_table(data: dict, headers: list = None):
|
|
28
|
+
if headers:
|
|
29
|
+
print(tabulate.tabulate(data, headers=headers))
|
|
30
|
+
else:
|
|
31
|
+
print(tabulate.tabulate(data, headers="keys"))
|
|
32
|
+
|
|
33
|
+
def to_date(epoch: int) -> str:
|
|
34
|
+
date = datetime.datetime.fromtimestamp(epoch/1000)
|
|
35
|
+
return date.strftime("%d/%m/%Y, %H:%M:%S")
|
|
36
|
+
|
|
37
|
+
def now():
|
|
38
|
+
return round(time.time() * 1000)
|
|
39
|
+
|
|
40
|
+
def populate_resources(raw: dict, output: dict):
|
|
41
|
+
cpu_list = [r for r in raw.get("resources", list()) if r.get("type", "") == "CPU"]
|
|
42
|
+
if len(cpu_list) > 0:
|
|
43
|
+
output["CPU"] = ", ".join(["NUMA Node %s: Cores: %s" % (key, value) for (key, value) in cpu_list[0].get("cores", dict()).items()])
|
|
44
|
+
memory_list = [r for r in raw.get("resources", list()) if r.get("type", "") == "MEMORY"]
|
|
45
|
+
if len(memory_list) > 0:
|
|
46
|
+
output["Memory (MB)"] = ", ".join(["NUMA Node %s: Cores: %s" % (key, value) for (key, value) in memory_list[0].get("memoryInMB", dict()).items()])
|
|
47
|
+
|
|
48
|
+
def list_logs(drove_client: droveclient.DroveClient, prefix: str, domain: str, id: str):
|
|
49
|
+
data = drove_client.get_raw("/apis/v1/logfiles/{prefix}/{domain}/{id}/list".format(prefix=prefix, domain=domain, id=id))
|
|
50
|
+
print_dict(data)
|
|
51
|
+
|
|
52
|
+
def tail_log(drove_client: droveclient.DroveClient, prefix: str, domain: str, id: str, file_name: str):
|
|
53
|
+
offset:int = -1
|
|
54
|
+
old_offset:int = -1
|
|
55
|
+
while True:
|
|
56
|
+
old_offset = offset
|
|
57
|
+
data = drove_client.get_raw("/apis/v1/logfiles/{prefix}/{domain}/{id}/read/{name}?offset={offset}"
|
|
58
|
+
.format(prefix=prefix, domain=domain, id=id, name=file_name, offset=offset))
|
|
59
|
+
data_length = len(data["data"])
|
|
60
|
+
if old_offset == -1:
|
|
61
|
+
offset = int(data["offset"])
|
|
62
|
+
else:
|
|
63
|
+
offset = offset + data_length
|
|
64
|
+
|
|
65
|
+
if data_length == 0:
|
|
66
|
+
time.sleep(1) #Nothing available right now .. try a bit later. TODO: binary backoff overkill here?
|
|
67
|
+
else:
|
|
68
|
+
print(data["data"], end='')
|
|
69
|
+
|
|
70
|
+
def download_log(drove_client: droveclient.DroveClient, prefix: str, domain: str, id: str, file_name: str, outfilename: str):
|
|
71
|
+
print("Downloading log to: " + outfilename)
|
|
72
|
+
size = drove_client.get_to_file("/apis/v1/logfiles/{prefix}/{domain}/{id}/download/{name}"
|
|
73
|
+
.format(prefix=prefix, domain=domain, id=id, name=file_name), outfilename)
|
|
74
|
+
print("Downloaded size: {size:,} bytes".format(size = size))
|
|
75
|
+
|
|
76
|
+
def print_drove_error(e: droveclient.DroveException, print_raw: bool):
|
|
77
|
+
printed = False
|
|
78
|
+
if e.api_response != None:
|
|
79
|
+
if "data" in e.api_response:
|
|
80
|
+
if "message" in e.api_response:
|
|
81
|
+
print("Error: [{code} - {status}] {message}.".format(message=e.api_response["message"], code=e.status_code, status=e.api_response["status"]))
|
|
82
|
+
printed = True
|
|
83
|
+
if "validationErrors" in e.api_response["data"]:
|
|
84
|
+
print("Validation errors:")
|
|
85
|
+
[print(" - " + err) for err in e.api_response["data"]["validationErrors"]]
|
|
86
|
+
printed = True
|
|
87
|
+
if print_raw:
|
|
88
|
+
print("Raw Response: ")
|
|
89
|
+
print_json(e.api_response)
|
|
90
|
+
if printed == False:
|
|
91
|
+
print("Error making Drove call: {error}".format(error = str(e)))
|
plugins/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import droveclient
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from importlib import util
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DrovePlugin:
|
|
11
|
+
plugins = []
|
|
12
|
+
|
|
13
|
+
def __init_subclass__(cls, **kwargs):
|
|
14
|
+
super().__init_subclass__(**kwargs)
|
|
15
|
+
cls.plugins.append(cls)
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.drove_client: droveclient.DroveClient = None
|
|
19
|
+
self.parser: argparse.ArgumentParser = None
|
|
20
|
+
|
|
21
|
+
def populate_options(self, drove_client: droveclient.DroveClient, subparser: argparse.ArgumentParser):
|
|
22
|
+
self.drove_client = drove_client
|
|
23
|
+
subparser.set_defaults(func=self.process)
|
|
24
|
+
self.parser = subparser
|
|
25
|
+
|
|
26
|
+
def process(self, options: SimpleNamespace):
|
|
27
|
+
self.parser.print_help()
|
|
28
|
+
exit(-1)
|
|
29
|
+
|
|
30
|
+
def load_module(path):
|
|
31
|
+
name = os.path.split(path)[-1]
|
|
32
|
+
spec = util.spec_from_file_location(name, path)
|
|
33
|
+
module = util.module_from_spec(spec)
|
|
34
|
+
spec.loader.exec_module(module)
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
path = os.path.abspath(__file__)
|
|
38
|
+
dirpath = os.path.dirname(path)
|
|
39
|
+
|
|
40
|
+
for fname in os.listdir(dirpath):
|
|
41
|
+
# Load only "real modules"
|
|
42
|
+
if not fname.startswith('.') and \
|
|
43
|
+
not fname.startswith('__') and fname.endswith('.py'):
|
|
44
|
+
try:
|
|
45
|
+
load_module(os.path.join(dirpath, fname))
|
|
46
|
+
except Exception:
|
|
47
|
+
traceback.print_exc()
|
plugins/appinstances.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import droveclient
|
|
3
|
+
import droveutils
|
|
4
|
+
import plugins
|
|
5
|
+
import tenacity
|
|
6
|
+
|
|
7
|
+
from operator import itemgetter
|
|
8
|
+
from tenacity import retry
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
class Applications(plugins.DrovePlugin):
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def populate_options(self, drove_client: droveclient.DroveClient, subparser: argparse.ArgumentParser):
|
|
16
|
+
parser = subparser.add_parser("appinstances", help="Drove application instance related commands")
|
|
17
|
+
|
|
18
|
+
commands = parser.add_subparsers(help="Available commands for application management")
|
|
19
|
+
|
|
20
|
+
sub_parser = commands.add_parser("list", help="List all application instances")
|
|
21
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
22
|
+
sub_parser.add_argument("--old", "-o", help="Show old instances", action="store_true")
|
|
23
|
+
sub_parser.add_argument("--sort", "-s", help="Sort output by column", type=int, choices=range(0, 6), default = 0)
|
|
24
|
+
sub_parser.add_argument("--reverse", "-r", help="Sort in reverse order", action="store_true")
|
|
25
|
+
sub_parser.set_defaults(func=self.list_instances)
|
|
26
|
+
|
|
27
|
+
sub_parser = commands.add_parser("info", help="Print details for an application instance")
|
|
28
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
29
|
+
sub_parser.add_argument("instance_id", metavar="instance-id", help="Application Instance ID")
|
|
30
|
+
sub_parser.set_defaults(func=self.show_instance)
|
|
31
|
+
|
|
32
|
+
sub_parser = commands.add_parser("logs", help="Print list of logs for application instance")
|
|
33
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
34
|
+
sub_parser.add_argument("instance_id", metavar="instance-id", help="Application Instance ID")
|
|
35
|
+
sub_parser.set_defaults(func=self.show_logs_list)
|
|
36
|
+
|
|
37
|
+
sub_parser = commands.add_parser("tail", help="Tail log for application instance")
|
|
38
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
39
|
+
sub_parser.add_argument("instance_id", metavar="instance-id", help="Application Instance ID")
|
|
40
|
+
sub_parser.add_argument("--log", "-l", default = "output.log", help="Log filename to tail. Default is to tail output.log")
|
|
41
|
+
sub_parser.set_defaults(func=self.log_tail)
|
|
42
|
+
|
|
43
|
+
sub_parser = commands.add_parser("download", help="Download log for application instance")
|
|
44
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
45
|
+
sub_parser.add_argument("instance_id", metavar="instance-id", help="Application Instance ID")
|
|
46
|
+
sub_parser.add_argument("applogfile", help="Log filename to download")
|
|
47
|
+
sub_parser.add_argument("--out", "-o", help="Filename to download to. Default is the same filename as provided.")
|
|
48
|
+
sub_parser.set_defaults(func=self.log_download)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
sub_parser = commands.add_parser("replace", help="Replace specific app instances with fresh instances")
|
|
52
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
53
|
+
sub_parser.add_argument("instance_ids", nargs="+", metavar="instance-id", help="Application Instance IDs")
|
|
54
|
+
sub_parser.add_argument("--parallelism", "-p", help="Number of parallel threads to be used to execute operation", type=int, default = 1)
|
|
55
|
+
sub_parser.add_argument("--timeout", "-t", help="Timeout for the operation on the cluster", type=str, default = "5m")
|
|
56
|
+
sub_parser.add_argument("--wait", "-w", help="Wait to ensure all instances are replaced", default=False, action="store_true")
|
|
57
|
+
sub_parser.set_defaults(func=self.replace)
|
|
58
|
+
|
|
59
|
+
sub_parser = commands.add_parser("kill", help="Kill specific app instances")
|
|
60
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
61
|
+
sub_parser.add_argument("instance_ids", nargs="+", metavar="instance-id", help="Application Instance IDs")
|
|
62
|
+
sub_parser.add_argument("--parallelism", "-p", help="Number of parallel threads to be used to execute operation", type=int, default = 1)
|
|
63
|
+
sub_parser.add_argument("--timeout", "-t", help="Timeout for the operation on the cluster", type=str, default = "5m")
|
|
64
|
+
sub_parser.add_argument("--wait", "-w", help="Wait to ensure all instances are killed", default=False, action="store_true")
|
|
65
|
+
sub_parser.set_defaults(func=self.kill)
|
|
66
|
+
|
|
67
|
+
# sub_parser = commands.add_parser("create", help="Create application")
|
|
68
|
+
# sub_parser.add_argument("definition", help="JSON application definition")
|
|
69
|
+
|
|
70
|
+
super().populate_options(drove_client, parser)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def list_instances(self, options: SimpleNamespace):
|
|
74
|
+
api = "/apis/v1/applications/{app_id}/instances"
|
|
75
|
+
if options.old:
|
|
76
|
+
api = "/apis/v1/applications/{app_id}/instances/old"
|
|
77
|
+
data = self.drove_client.get(api.format(app_id = options.app_id))
|
|
78
|
+
#headers = ["Instance ID", "Executor", "CPU", "Memory(MB)", "State", "Error Message", "Created", "Last Updated"]
|
|
79
|
+
headers = ["Instance ID", "Executor Host", "State", "Error Message", "Created", "Last Updated"]
|
|
80
|
+
rows = []
|
|
81
|
+
for instance in data:
|
|
82
|
+
instance_row = []
|
|
83
|
+
instance_row.append(instance["instanceId"])
|
|
84
|
+
try:
|
|
85
|
+
instance_row.append(instance["localInfo"]["hostname"])
|
|
86
|
+
except KeyError:
|
|
87
|
+
instance_row.append("")
|
|
88
|
+
instance_row.append(instance["state"])
|
|
89
|
+
instance_row.append(instance["errorMessage"])
|
|
90
|
+
instance_row.append(droveutils.to_date(instance["created"]))
|
|
91
|
+
instance_row.append(droveutils.to_date(instance["updated"]))
|
|
92
|
+
|
|
93
|
+
rows.append(instance_row)
|
|
94
|
+
rows = sorted(rows, key=itemgetter(options.sort), reverse=options.reverse)
|
|
95
|
+
droveutils.print_table(headers, rows)
|
|
96
|
+
|
|
97
|
+
def show_instance(self, options):
|
|
98
|
+
raw = self.drove_client.get("/apis/v1/applications/{app_id}/instances/{instance_id}".format(app_id = options.app_id, instance_id=options.instance_id))
|
|
99
|
+
data = dict()
|
|
100
|
+
data["Instance ID"] = raw["instanceId"]
|
|
101
|
+
data["App ID"] = raw["appId"]
|
|
102
|
+
data["State"] = raw["state"]
|
|
103
|
+
data["Host"] = raw.get("localInfo", dict()).get("hostname", "")
|
|
104
|
+
droveutils.populate_resources(raw, data)
|
|
105
|
+
ports = raw.get("localInfo", dict()).get("ports", dict())
|
|
106
|
+
data["Ports"] = ", ".join(["%s: %s" % (key, "{containerPort}->{hostPort} ({portType})".format_map(value)) for (key, value) in ports.items()])
|
|
107
|
+
data["Metadata"] = ", ".join(["%s: %s" % (key,value) for (key, value) in raw.get("metadata", dict())])
|
|
108
|
+
data["Error Message"] = raw.get("errorMessage", "").strip('\n')
|
|
109
|
+
data["Created"] = droveutils.to_date(raw.get("created"))
|
|
110
|
+
data["Last Updated"] = droveutils.to_date(raw.get("updated"))
|
|
111
|
+
|
|
112
|
+
droveutils.print_dict(data)
|
|
113
|
+
|
|
114
|
+
def show_logs_list(self, options):
|
|
115
|
+
droveutils.list_logs(self.drove_client, "applications", options.app_id, options.instance_id)
|
|
116
|
+
|
|
117
|
+
def log_tail(self, options):
|
|
118
|
+
droveutils.tail_log(self.drove_client, "applications", options.app_id, options.instance_id, options.log)
|
|
119
|
+
|
|
120
|
+
def log_download(self, options):
|
|
121
|
+
filename = options.applogfile
|
|
122
|
+
if options.out and len(options.out) > 0:
|
|
123
|
+
filename = options.out
|
|
124
|
+
droveutils.download_log(self.drove_client, "applications", options.app_id, options.instance_id, options.applogfile, filename)
|
|
125
|
+
|
|
126
|
+
def replace(self, options):
|
|
127
|
+
operation = {
|
|
128
|
+
"type": "REPLACE_INSTANCES",
|
|
129
|
+
"appId": options.app_id,
|
|
130
|
+
"instanceIds": options.instance_ids,
|
|
131
|
+
"opSpec": {
|
|
132
|
+
"timeout": options.timeout,
|
|
133
|
+
"parallelism": options.parallelism,
|
|
134
|
+
"failureStrategy": "STOP"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
138
|
+
if options.wait:
|
|
139
|
+
self.ensure_replaced(options.app_id, set(options.instance_ids))
|
|
140
|
+
print("All instances replaced")
|
|
141
|
+
else:
|
|
142
|
+
print("Instance(s) replace command accepted.")
|
|
143
|
+
|
|
144
|
+
def kill(self, options):
|
|
145
|
+
operation = {
|
|
146
|
+
"type": "STOP_INSTANCES",
|
|
147
|
+
"appId": options.app_id,
|
|
148
|
+
"instanceIds": options.instance_ids,
|
|
149
|
+
"skipRespawn": True,
|
|
150
|
+
"opSpec": {
|
|
151
|
+
"timeout": options.timeout,
|
|
152
|
+
"parallelism": options.parallelism,
|
|
153
|
+
"failureStrategy": "STOP"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
157
|
+
if options.wait:
|
|
158
|
+
self.ensure_replaced(options.app_id, set(options.instance_ids))
|
|
159
|
+
print("All instances replaced")
|
|
160
|
+
else:
|
|
161
|
+
print("Instance(s) kill command accepted.")
|
|
162
|
+
|
|
163
|
+
@retry(wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
164
|
+
retry=tenacity.retry_if_result(lambda x: x == False))
|
|
165
|
+
def ensure_replaced(self, app_id: str, existing: set) -> bool:
|
|
166
|
+
healthy = self.drove_client.app_instances(app_id, False)
|
|
167
|
+
overlap = len(existing.intersection(healthy))
|
|
168
|
+
print("Remaining old instance count: {overlap}".format(overlap = overlap))
|
|
169
|
+
return overlap == 0
|