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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ drove=drove:run
3
+
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()
@@ -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