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
plugins/applications.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import droveclient
|
|
3
|
+
import droveutils
|
|
4
|
+
import json
|
|
5
|
+
import plugins
|
|
6
|
+
import tenacity
|
|
7
|
+
|
|
8
|
+
from operator import itemgetter
|
|
9
|
+
from tenacity import retry
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
|
|
12
|
+
class Applications(plugins.DrovePlugin):
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def populate_options(self, drove_client: droveclient.DroveClient, subparser: argparse.ArgumentParser):
|
|
17
|
+
parser = subparser.add_parser("apps", help="Drove application related commands")
|
|
18
|
+
|
|
19
|
+
commands = parser.add_subparsers(help="Available commands for application management")
|
|
20
|
+
|
|
21
|
+
sub_parser = commands.add_parser("list", help="List all applications")
|
|
22
|
+
sub_parser.add_argument("--sort", "-s", help="Sort output by column", type=int, choices=range(0, 9), default = 0)
|
|
23
|
+
sub_parser.add_argument("--reverse", "-r", help="Sort in reverse order", action="store_true")
|
|
24
|
+
sub_parser.set_defaults(func=self.list_apps)
|
|
25
|
+
|
|
26
|
+
sub_parser = commands.add_parser("summary", help="Show a summary for an application")
|
|
27
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
28
|
+
sub_parser.set_defaults(func=self.show_summary)
|
|
29
|
+
|
|
30
|
+
sub_parser = commands.add_parser("spec", help="Print the raw json spec for an application")
|
|
31
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
32
|
+
sub_parser.set_defaults(func=self.show_spec)
|
|
33
|
+
|
|
34
|
+
sub_parser = commands.add_parser("create", help="Create application on cluster")
|
|
35
|
+
sub_parser.add_argument("spec_file", metavar="spec-file", help="JSON spec file for the application")
|
|
36
|
+
sub_parser.set_defaults(func=self.create_app)
|
|
37
|
+
|
|
38
|
+
sub_parser = commands.add_parser("destroy", help="Destroy an app with zero instances")
|
|
39
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
40
|
+
sub_parser.set_defaults(func=self.destroy_app)
|
|
41
|
+
|
|
42
|
+
sub_parser = commands.add_parser("deploy", help="Deploy new app instances.")
|
|
43
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
44
|
+
sub_parser.add_argument("instances", metavar="instances", type=int, help="Number of new instances to be created")
|
|
45
|
+
sub_parser.add_argument("--parallelism", "-p", help="Number of parallel threads to be used to execute operation", type=int, default = 1)
|
|
46
|
+
sub_parser.add_argument("--timeout", "-t", help="Timeout for the operation on the cluster", type=str, default = "5m")
|
|
47
|
+
sub_parser.add_argument("--wait", "-w", help="Wait to ensure instance count is reached", default=False, action="store_true")
|
|
48
|
+
sub_parser.set_defaults(func=self.deploy_app)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
sub_parser = commands.add_parser("scale", help="Scale app to required instances. Will increase or decrease instances on the cluster to match this number")
|
|
52
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
53
|
+
sub_parser.add_argument("instances", metavar="instances", type=int, help="Number of instances. Setting this to 0 will suspend the app")
|
|
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 instance count is reached", default=False, action="store_true")
|
|
57
|
+
sub_parser.set_defaults(func=self.scale_app)
|
|
58
|
+
|
|
59
|
+
sub_parser = commands.add_parser("suspend", help="Suspend the app")
|
|
60
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
61
|
+
sub_parser.add_argument("--parallelism", "-p", help="Number of parallel threads to be used to execute operation", type=int, default = 1)
|
|
62
|
+
sub_parser.add_argument("--timeout", "-t", help="Timeout for the operation on the cluster", type=str, default = "5m")
|
|
63
|
+
sub_parser.add_argument("--wait", "-w", help="Wait to ensure all instances are suspended", default=False, action="store_true")
|
|
64
|
+
sub_parser.set_defaults(func=self.suspend_app)
|
|
65
|
+
|
|
66
|
+
sub_parser = commands.add_parser("restart", help="Restart am existing app instances.")
|
|
67
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
68
|
+
sub_parser.add_argument("--parallelism", "-p", help="Number of parallel threads to be used to execute operation", type=int, default = 1)
|
|
69
|
+
sub_parser.add_argument("--timeout", "-t", help="Timeout for the operation on the cluster", type=str, default = "5m")
|
|
70
|
+
sub_parser.add_argument("--wait", "-w", help="Wait to ensure all instances are replaced", default=False, action="store_true")
|
|
71
|
+
sub_parser.set_defaults(func=self.restart_app)
|
|
72
|
+
|
|
73
|
+
sub_parser = commands.add_parser("cancelop", help="Cancel current operation")
|
|
74
|
+
sub_parser.add_argument("app_id", metavar="app-id", help="Application ID")
|
|
75
|
+
sub_parser.set_defaults(func=self.cancel_app_operation)
|
|
76
|
+
|
|
77
|
+
super().populate_options(drove_client, parser)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def list_apps(self, options: SimpleNamespace):
|
|
81
|
+
data = self.drove_client.get('/apis/v1/applications')
|
|
82
|
+
app_rows = []
|
|
83
|
+
for app_id, app_data in data.items():
|
|
84
|
+
row = []
|
|
85
|
+
row.append(app_id)
|
|
86
|
+
row.append(app_data["name"])
|
|
87
|
+
row.append(app_data["state"])
|
|
88
|
+
row.append(app_data["totalCPUs"])
|
|
89
|
+
row.append(app_data["totalMemory"])
|
|
90
|
+
row.append(app_data["requiredInstances"])
|
|
91
|
+
row.append(app_data["healthyInstances"])
|
|
92
|
+
row.append(droveutils.to_date(app_data["created"]))
|
|
93
|
+
row.append(droveutils.to_date(app_data["updated"]))
|
|
94
|
+
|
|
95
|
+
app_rows.append(row)
|
|
96
|
+
|
|
97
|
+
app_rows = sorted(app_rows, key=itemgetter(options.sort), reverse=options.reverse)
|
|
98
|
+
|
|
99
|
+
headers = ["Id", "Name", "State", "Total CPU", "Total Memory(MB)", "Required Instances", "Healthy Instances", "Created", "Updated"]
|
|
100
|
+
droveutils.print_table(headers, app_rows)
|
|
101
|
+
|
|
102
|
+
def show_summary(self, options: SimpleNamespace):
|
|
103
|
+
data = self.drove_client.get("/apis/v1/applications/{app_id}".format(app_id = options.app_id))
|
|
104
|
+
droveutils.print_dict(data)
|
|
105
|
+
|
|
106
|
+
def show_spec(self, options: SimpleNamespace):
|
|
107
|
+
data = self.drove_client.get("/apis/v1/applications/{app_id}/spec".format(app_id = options.app_id))
|
|
108
|
+
droveutils.print_json(data)
|
|
109
|
+
|
|
110
|
+
def create_app(self, options: SimpleNamespace):
|
|
111
|
+
try:
|
|
112
|
+
with open(options.spec_file, 'r') as fp:
|
|
113
|
+
spec = json.load(fp)
|
|
114
|
+
operation = {
|
|
115
|
+
"type": "CREATE",
|
|
116
|
+
"spec": spec,
|
|
117
|
+
"opSpec": {
|
|
118
|
+
"timeout": "5m",
|
|
119
|
+
"parallelism": 1,
|
|
120
|
+
"failureStrategy": "STOP"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
124
|
+
print("Application created with app id: {appid}".format(appid=data["appId"]))
|
|
125
|
+
except (OSError, IOError) as e:
|
|
126
|
+
print("Error creating application. Error: " + str(e))
|
|
127
|
+
|
|
128
|
+
def destroy_app(self, options: SimpleNamespace):
|
|
129
|
+
operation = {
|
|
130
|
+
"type": "DESTROY",
|
|
131
|
+
"appId": options.app_id,
|
|
132
|
+
"opSpec": {
|
|
133
|
+
"timeout": "5m",
|
|
134
|
+
"parallelism": 1,
|
|
135
|
+
"failureStrategy": "STOP"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
139
|
+
print("Application destroyed")
|
|
140
|
+
|
|
141
|
+
def scale_app(self, options: SimpleNamespace):
|
|
142
|
+
operation = {
|
|
143
|
+
"type": "SCALE",
|
|
144
|
+
"appId": options.app_id,
|
|
145
|
+
"requiredInstances": options.instances,
|
|
146
|
+
"opSpec": {
|
|
147
|
+
"timeout": options.timeout,
|
|
148
|
+
"parallelism": options.parallelism,
|
|
149
|
+
"failureStrategy": "STOP"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
153
|
+
if options.wait:
|
|
154
|
+
print("Waiting till required scale is reached")
|
|
155
|
+
self.ensure_count(options.app_id, options.instances)
|
|
156
|
+
print("Required number of instances reached")
|
|
157
|
+
else:
|
|
158
|
+
print("Application scaling command accepted. Please use appinstances comand or the UI to check status of deployment")
|
|
159
|
+
|
|
160
|
+
def suspend_app(self, options: SimpleNamespace):
|
|
161
|
+
operation = {
|
|
162
|
+
"type": "SUSPEND",
|
|
163
|
+
"appId": options.app_id,
|
|
164
|
+
"opSpec": {
|
|
165
|
+
"timeout": options.timeout,
|
|
166
|
+
"parallelism": options.parallelism,
|
|
167
|
+
"failureStrategy": "STOP"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
171
|
+
if options.wait:
|
|
172
|
+
print("Waiting till all instances shut down")
|
|
173
|
+
self.ensure_count(options.app_id, 0)
|
|
174
|
+
print("All instances suspended")
|
|
175
|
+
else:
|
|
176
|
+
print("Application suspend command accepted.")
|
|
177
|
+
|
|
178
|
+
def deploy_app(self, options: SimpleNamespace):
|
|
179
|
+
operation = {
|
|
180
|
+
"type": "START_INSTANCES",
|
|
181
|
+
"appId": options.app_id,
|
|
182
|
+
"instances": options.instances,
|
|
183
|
+
"opSpec": {
|
|
184
|
+
"timeout": options.timeout,
|
|
185
|
+
"parallelism": options.parallelism,
|
|
186
|
+
"failureStrategy": "STOP"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
existing_count = 0 if options.wait == False else len(self.drove_client.app_instances(options.app_id))
|
|
190
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
191
|
+
if options.wait:
|
|
192
|
+
print("Waiting till required scale is reached")
|
|
193
|
+
self.ensure_count(options.app_id, existing_count + options.instances)
|
|
194
|
+
print("Required number of instances reached")
|
|
195
|
+
else:
|
|
196
|
+
print("Application deployment command accepted. Please use appinstances comand or the UI to check status of deployment")
|
|
197
|
+
|
|
198
|
+
def restart_app(self, options: SimpleNamespace):
|
|
199
|
+
operation = {
|
|
200
|
+
"type": "REPLACE_INSTANCES",
|
|
201
|
+
"appId": options.app_id,
|
|
202
|
+
"opSpec": {
|
|
203
|
+
"timeout": options.timeout,
|
|
204
|
+
"parallelism": options.parallelism,
|
|
205
|
+
"failureStrategy": "STOP"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
existing = [] if options.wait == False else self.drove_client.app_instances(options.app_id)
|
|
209
|
+
data = self.drove_client.post("/apis/v1/applications/operations", operation)
|
|
210
|
+
if options.wait:
|
|
211
|
+
self.ensure_replaced(options.app_id, set(existing))
|
|
212
|
+
print("All instances replaced")
|
|
213
|
+
else:
|
|
214
|
+
print("Application restart command accepted.")
|
|
215
|
+
|
|
216
|
+
def cancel_app_operation(self, options: SimpleNamespace):
|
|
217
|
+
data = self.drove_client.post("/apis/v1/operations/{appId}/cancel".format(appId=options.app_id), None, False)
|
|
218
|
+
print("Operation cancellation request registered :" + data["message"])
|
|
219
|
+
|
|
220
|
+
@retry(wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
221
|
+
retry=tenacity.retry_if_result(lambda x: x == False))
|
|
222
|
+
def ensure_count(self, app_id: str, instances: int) -> bool:
|
|
223
|
+
healthy = len(self.drove_client.app_instances(app_id))
|
|
224
|
+
print("Healthy instances count: {count}".format(count=healthy))
|
|
225
|
+
return healthy == instances
|
|
226
|
+
|
|
227
|
+
@retry(wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
228
|
+
retry=tenacity.retry_if_result(lambda x: x == False))
|
|
229
|
+
def ensure_replaced(self, app_id: str, existing: set) -> bool:
|
|
230
|
+
healthy = self.drove_client.app_instances(app_id)
|
|
231
|
+
overlap = len(existing.intersection(healthy))
|
|
232
|
+
print("Remaining old instance count: {overlap}".format(overlap = overlap))
|
|
233
|
+
return overlap == 0
|
plugins/cluster.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from operator import itemgetter
|
|
3
|
+
import droveclient
|
|
4
|
+
import droveutils
|
|
5
|
+
import json
|
|
6
|
+
import plugins
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
class Cluster(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("cluster", help="Drove cluster related commands")
|
|
17
|
+
|
|
18
|
+
commands = parser.add_subparsers(help="Available commands for cluster management")
|
|
19
|
+
|
|
20
|
+
sub_parser = commands.add_parser("ping", help="Ping the cluster")
|
|
21
|
+
sub_parser.set_defaults(func=self.ping)
|
|
22
|
+
|
|
23
|
+
sub_parser = commands.add_parser("summary", help="Show cluster summary")
|
|
24
|
+
sub_parser.set_defaults(func=self.show_summary)
|
|
25
|
+
|
|
26
|
+
sub_parser = commands.add_parser("leader", help="Show leader for cluster")
|
|
27
|
+
sub_parser.set_defaults(func=self.show_leader)
|
|
28
|
+
|
|
29
|
+
sub_parser = commands.add_parser("endpoints", help="Show all exposed endpoints")
|
|
30
|
+
sub_parser.add_argument("--vhost", "-v", help="Show details only for the specific vhost")
|
|
31
|
+
|
|
32
|
+
sub_parser.set_defaults(func=self.show_endpoints)
|
|
33
|
+
|
|
34
|
+
sub_parser = commands.add_parser("events", help="Events on the cluster")
|
|
35
|
+
sub_parser.add_argument("--follow", "-f", help="Follow events (Press CTRL-C to kill)", action="store_true")
|
|
36
|
+
sub_parser.add_argument("--type", "-t", help="Output events of only the matching type")
|
|
37
|
+
sub_parser.add_argument("--count", "-c", help="Fetch <count> events at a time.", default=1024, type=int)
|
|
38
|
+
sub_parser.add_argument("--textfmt", "-s", help="Use the format string to print message", type=str, default="{type: <25} | {id: <36} | {time: <20} | {metadata}")
|
|
39
|
+
sub_parser.set_defaults(func=self.handle_events)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# maintenance_parser = commands.add_parser("maintenance-on", help="Set cluster to maintenance mode")
|
|
43
|
+
# maintenance_parser.set_defaults(func=self.set_maintenance)
|
|
44
|
+
|
|
45
|
+
# maintenance_parser = commands.add_parser("maintenance-off", help="Removed maintenance mode on cluster")
|
|
46
|
+
# maintenance_parser.set_defaults(func=self.unset_maintenance)
|
|
47
|
+
|
|
48
|
+
# executors_command = commands.add_parser("executors", help="List executors")
|
|
49
|
+
# executors_command.add_argument("--list", "-l", help="List executors", action="store_true")
|
|
50
|
+
# executors_command.add_argument("--info", "-i", dest='executor_id', help="Get detailed info about executor")
|
|
51
|
+
|
|
52
|
+
super().populate_options(drove_client, parser)
|
|
53
|
+
|
|
54
|
+
def ping(self, options: SimpleNamespace):
|
|
55
|
+
try:
|
|
56
|
+
self.drove_client.get("/apis/v1/ping")
|
|
57
|
+
print("Cluster ping successful")
|
|
58
|
+
except droveclient.DroveException as e:
|
|
59
|
+
print("Error pinging drove cluster: status: {status} message: {message} raw: {raw}"
|
|
60
|
+
.format(status = e.status_code, message = str(e), raw = e.raw))
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print("Error pinging drove cluster: " + str(e))
|
|
63
|
+
|
|
64
|
+
def show_summary(self, options: SimpleNamespace):
|
|
65
|
+
raw = self.drove_client.get("/apis/v1/cluster")
|
|
66
|
+
data = dict()
|
|
67
|
+
data["State"] = raw["state"]
|
|
68
|
+
data["Leader Controller"] = raw.get("leader", "")
|
|
69
|
+
data["Cores"] = "Utilization: {util:.0%} (Total: {total} Used: {used} Free: {free})".format(util = float(raw["usedCores"]/raw["totalCores"]), total=raw["totalCores"], used = raw["usedCores"], free = raw["freeCores"])
|
|
70
|
+
data["Memory"] = "Utilization: {util:.0%} (Total: {total:,} MB Used: {used:,} MB Free: {free:,} MB)".format(util = float(raw["usedMemory"]/raw["totalMemory"]), total = raw["totalMemory"], used = raw["usedMemory"], free = raw["freeMemory"])
|
|
71
|
+
data["Number of live executors"] = raw["numExecutors"]
|
|
72
|
+
data["Applications"] = "Active: {active:,} Total: {total:,}".format(total = raw["numApplications"], active = raw["numActiveApplications"])
|
|
73
|
+
|
|
74
|
+
droveutils.print_dict(data)
|
|
75
|
+
|
|
76
|
+
def show_leader(self, options: SimpleNamespace):
|
|
77
|
+
data = self.drove_client.get("/apis/v1/cluster")
|
|
78
|
+
try:
|
|
79
|
+
print("Cluster leader: " + data["leader"])
|
|
80
|
+
except KeyError:
|
|
81
|
+
print("Cluster has no leader")
|
|
82
|
+
|
|
83
|
+
def show_endpoints(self, options: SimpleNamespace):
|
|
84
|
+
raw = self.drove_client.get("/apis/v1/endpoints")
|
|
85
|
+
data = sorted(raw, key=itemgetter('vhost', 'appId'))
|
|
86
|
+
if options.vhost:
|
|
87
|
+
data = [d for d in data if d['vhost'] == options.vhost]
|
|
88
|
+
droveutils.print_dict({"endpoints" : data})
|
|
89
|
+
|
|
90
|
+
def handle_events(self, options: SimpleNamespace):
|
|
91
|
+
currtime = 0
|
|
92
|
+
while True:
|
|
93
|
+
events = self.drove_client.get("/apis/v1/cluster/events?size={size}&lastSyncTime={time}".format(size=options.count, time=currtime))
|
|
94
|
+
if options.type:
|
|
95
|
+
events = [e for e in events if e['type'] == options.type]
|
|
96
|
+
if len(events) > 0:
|
|
97
|
+
print("\n".join([self.convert_event(options.textfmt, e) for e in events]))
|
|
98
|
+
if not options.follow:
|
|
99
|
+
break
|
|
100
|
+
currtime = droveutils.now()
|
|
101
|
+
time.sleep(1)
|
|
102
|
+
|
|
103
|
+
def set_maintenance(self, options: SimpleNamespace):
|
|
104
|
+
print("Maintenance mode set")
|
|
105
|
+
|
|
106
|
+
def unset_maintenance(self, options: SimpleNamespace):
|
|
107
|
+
print("Maintenance mode unset")
|
|
108
|
+
|
|
109
|
+
def convert_event(self, format: str, event: dict) -> str:
|
|
110
|
+
data = dict()
|
|
111
|
+
data["type"] = event["type"]
|
|
112
|
+
data["id"] = event["id"]
|
|
113
|
+
data["time"] = droveutils.to_date(event["time"])
|
|
114
|
+
data["metadata"] = json.dumps(event["metadata"])
|
|
115
|
+
|
|
116
|
+
return format.format_map(data)
|
plugins/executors.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from operator import itemgetter
|
|
3
|
+
import droveclient
|
|
4
|
+
import droveutils
|
|
5
|
+
import json
|
|
6
|
+
import plugins
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
class Executors(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("executor", help="Drove cluster executor related commands")
|
|
17
|
+
|
|
18
|
+
commands = parser.add_subparsers(help="Available commands for cluster executor management")
|
|
19
|
+
|
|
20
|
+
sub_parser = commands.add_parser("list", help="List all executors")
|
|
21
|
+
sub_parser.set_defaults(func=self.list)
|
|
22
|
+
|
|
23
|
+
sub_parser = commands.add_parser("info", help="Show details about executor")
|
|
24
|
+
sub_parser.add_argument("executor_id", metavar="executor-id", help="Executor id for which info is to be shown")
|
|
25
|
+
sub_parser.set_defaults(func=self.show_info)
|
|
26
|
+
|
|
27
|
+
sub_parser = commands.add_parser("appinstances", help="Show app instances running on this executor")
|
|
28
|
+
sub_parser.add_argument("executor_id", metavar="executor-id", help="Executor id for which info is to be shown")
|
|
29
|
+
sub_parser.add_argument("--sort", "-s", help="Sort output by column", type=int, choices=range(0, 6), default = 1)
|
|
30
|
+
sub_parser.add_argument("--reverse", "-r", help="Sort in reverse order", action="store_true")
|
|
31
|
+
sub_parser.set_defaults(func=self.show_appinstances)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
sub_parser = commands.add_parser("tasks", help="Show tasks running on this executor")
|
|
35
|
+
sub_parser.add_argument("executor_id", metavar="executor-id", help="Executor id for which info is to be shown")
|
|
36
|
+
sub_parser.add_argument("--sort", "-s", help="Sort output by column", type=int, choices=range(0, 6), default = 1)
|
|
37
|
+
sub_parser.add_argument("--reverse", "-r", help="Sort in reverse order", action="store_true")
|
|
38
|
+
sub_parser.set_defaults(func=self.show_tasks)
|
|
39
|
+
|
|
40
|
+
super().populate_options(drove_client, parser)
|
|
41
|
+
|
|
42
|
+
def list(self, options: SimpleNamespace):
|
|
43
|
+
raw = self.drove_client.get("/apis/v1/cluster/executors")
|
|
44
|
+
droveutils.print_dict_table(raw, headers={"executorId" : "Executor ID",
|
|
45
|
+
"hostname" : "Host",
|
|
46
|
+
"port" : "Port",
|
|
47
|
+
"transportType" : "Transport",
|
|
48
|
+
"freeCores" : "Free Cores",
|
|
49
|
+
"usedCores" : "Used Cores",
|
|
50
|
+
"freeMemory" : "Free Memory (MB)",
|
|
51
|
+
"usedMemory" : "Used Memory (MB)",
|
|
52
|
+
"tags" : "Tags",
|
|
53
|
+
"state" : "State"})
|
|
54
|
+
|
|
55
|
+
def show_info(self, options: SimpleNamespace):
|
|
56
|
+
raw = self.drove_client.get("/apis/v1/cluster/executors/{id}".format(id=options.executor_id))
|
|
57
|
+
# droveutils.print_dict(raw)
|
|
58
|
+
data = dict()
|
|
59
|
+
data["ID"] = raw["state"]["executorId"]
|
|
60
|
+
data["Host"] = raw["hostname"]
|
|
61
|
+
data["Port"] = raw["port"]
|
|
62
|
+
data["Transport"] = raw["transportType"]
|
|
63
|
+
cpu_nodes = dict()
|
|
64
|
+
for key, value in raw["state"]["cpus"]["freeCores"].items():
|
|
65
|
+
cpu_nodes["NUMA node " + str(key)] = "Free cores: " + ",".join(map(str, sorted(value)))
|
|
66
|
+
for key, value in raw["state"]["cpus"]["usedCores"].items():
|
|
67
|
+
cpu_nodes["NUMA node " + str(key)] = cpu_nodes.get("NUMA node " + str(key), "") + " Used cores: " + ",".join(map(str, sorted(value)))
|
|
68
|
+
memory_nodes = dict()
|
|
69
|
+
for key, value in raw["state"]["memory"]["freeMemory"].items():
|
|
70
|
+
memory_nodes["NUMA node " + str(key)] = "Free : {0: ,} MB".format(value)
|
|
71
|
+
for key, value in raw["state"]["memory"]["usedMemory"].items():
|
|
72
|
+
memory_nodes["NUMA node " + str(key)] = memory_nodes.get("NUMA node " + str(key), "") + " Used: {0: ,} MB".format(value)
|
|
73
|
+
|
|
74
|
+
data["Resources"] = { "CPU" : cpu_nodes, "Memory" : memory_nodes }
|
|
75
|
+
data["Blacklisted"] = raw["blacklisted"]
|
|
76
|
+
data["Tags"] = ",".join(raw["tags"])
|
|
77
|
+
data["Last Updated"] = droveutils.to_date(raw["updated"])
|
|
78
|
+
|
|
79
|
+
droveutils.print_dict(data)
|
|
80
|
+
|
|
81
|
+
def show_appinstances(self, options: SimpleNamespace):
|
|
82
|
+
raw = self.drove_client.get("/apis/v1/cluster/executors/{id}".format(id=options.executor_id))
|
|
83
|
+
headers = ["Instance ID", "App name", "App ID", "CPU", "Memory (MB)", "State", "Error Message", "Created", "Last Updated"]
|
|
84
|
+
rows = []
|
|
85
|
+
for instance in raw.get("instances", list()):
|
|
86
|
+
row = []
|
|
87
|
+
row.append(instance["instanceId"])
|
|
88
|
+
row.append(instance["appName"])
|
|
89
|
+
row.append(instance["appId"])
|
|
90
|
+
cpu_list = [r for r in instance.get("resources", list()) if r.get("type", "") == "CPU"]
|
|
91
|
+
if len(cpu_list) > 0:
|
|
92
|
+
row.append(len(cpu_list[0].get("cores", dict())))
|
|
93
|
+
memory_list = [r for r in instance.get("resources", list()) if r.get("type", "") == "MEMORY"]
|
|
94
|
+
if len(memory_list) > 0:
|
|
95
|
+
row.append("{0: ,}".format(sum(memory_list[0].get("memoryInMB", dict()).values())))
|
|
96
|
+
row.append(instance["state"])
|
|
97
|
+
row.append(instance["errorMessage"])
|
|
98
|
+
row.append(droveutils.to_date(instance["created"]))
|
|
99
|
+
row.append(droveutils.to_date(instance["updated"]))
|
|
100
|
+
|
|
101
|
+
rows.append(row)
|
|
102
|
+
rows = sorted(rows, key=itemgetter(options.sort), reverse=options.reverse)
|
|
103
|
+
droveutils.print_table(headers, rows)
|
|
104
|
+
|
|
105
|
+
def show_tasks(self, options: SimpleNamespace):
|
|
106
|
+
raw = self.drove_client.get("/apis/v1/cluster/executors/{id}".format(id=options.executor_id))
|
|
107
|
+
task_rows = []
|
|
108
|
+
for task in raw.get("tasks", list()):
|
|
109
|
+
if options.app and task["sourceAppName"] != options.app:
|
|
110
|
+
continue
|
|
111
|
+
row = []
|
|
112
|
+
row.append(task["instanceId"])
|
|
113
|
+
row.append(task["sourceAppName"])
|
|
114
|
+
row.append(task["taskId"])
|
|
115
|
+
row.append(task["state"])
|
|
116
|
+
cpu_list = [r for r in task.get("resources", list()) if r.get("type", "") == "CPU"]
|
|
117
|
+
if len(cpu_list) > 0:
|
|
118
|
+
row.append(len(cpu_list[0].get("cores", dict())))
|
|
119
|
+
memory_list = [r for r in task.get("resources", list()) if r.get("type", "") == "MEMORY"]
|
|
120
|
+
if len(memory_list) > 0:
|
|
121
|
+
row.append("{0: ,}".format(sum(memory_list[0].get("memoryInMB", dict()).values())))
|
|
122
|
+
row.append(droveutils.to_date(task["created"]))
|
|
123
|
+
row.append(droveutils.to_date(task["updated"]))
|
|
124
|
+
|
|
125
|
+
task_rows.append(row)
|
|
126
|
+
|
|
127
|
+
task_rows = sorted(task_rows, key=itemgetter(options.sort), reverse=options.reverse)
|
|
128
|
+
headers = ["Id", "Source App", "Task ID", "State", "CPU", "Memory(MB)", "Created", "Updated"]
|
|
129
|
+
droveutils.print_table(headers, task_rows)
|
plugins/tasks.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import droveclient
|
|
3
|
+
import droveutils
|
|
4
|
+
import json
|
|
5
|
+
import plugins
|
|
6
|
+
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from operator import itemgetter
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
class Tasks(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("tasks", help="Drove task related commands")
|
|
17
|
+
|
|
18
|
+
commands = parser.add_subparsers(help="Available commands for task management")
|
|
19
|
+
|
|
20
|
+
sub_parser = commands.add_parser("create", help="Spawn a new task on cluster")
|
|
21
|
+
sub_parser.add_argument("spec_file", metavar="spec-file", help="JSON spec file for the application")
|
|
22
|
+
sub_parser.set_defaults(func=self.create_task)
|
|
23
|
+
|
|
24
|
+
sub_parser = commands.add_parser("kill", help="Kill a running task")
|
|
25
|
+
sub_parser.add_argument("source_app_name", metavar="source-app-name", help="Source app name as specified in spec")
|
|
26
|
+
sub_parser.add_argument("task_id", metavar="task-id", help="ID of the task as specified in the spec")
|
|
27
|
+
sub_parser.set_defaults(func=self.kill_task)
|
|
28
|
+
|
|
29
|
+
sub_parser = commands.add_parser("list", help="List all active tasks")
|
|
30
|
+
sub_parser.add_argument("--app", "-a", help="Show tasks only for the given source app", type=str)
|
|
31
|
+
sub_parser.add_argument("--sort", "-s", help="Sort output by column", type=int, choices=range(0, 9), default = 0)
|
|
32
|
+
sub_parser.add_argument("--reverse", "-r", help="Sort in reverse order", action="store_true")
|
|
33
|
+
sub_parser.set_defaults(func=self.list_task)
|
|
34
|
+
|
|
35
|
+
sub_parser = commands.add_parser("show", help="Shows details about a task")
|
|
36
|
+
sub_parser.add_argument("source_app", metavar="source-app", help="Name of the Drove application that started the task")
|
|
37
|
+
sub_parser.add_argument("task_id", metavar="task-id", help="Task ID")
|
|
38
|
+
sub_parser.set_defaults(func=self.show_task)
|
|
39
|
+
|
|
40
|
+
sub_parser = commands.add_parser("logs", help="Print list of logs for task")
|
|
41
|
+
sub_parser.add_argument("source_app", metavar="source-app", help="Name of the Drove application that started the task")
|
|
42
|
+
sub_parser.add_argument("task_id", metavar="task-id", help="Task ID")
|
|
43
|
+
sub_parser.set_defaults(func=self.show_logs_list)
|
|
44
|
+
|
|
45
|
+
sub_parser = commands.add_parser("tail", help="Tail log for task")
|
|
46
|
+
sub_parser.add_argument("source_app", metavar="source-app", help="Name of the Drove application that started the task")
|
|
47
|
+
sub_parser.add_argument("task_id", metavar="task-id", help="Task ID")
|
|
48
|
+
sub_parser.add_argument("--file", "-f", default = "output.log", help="Log filename to tail. Default is to tail output.log")
|
|
49
|
+
sub_parser.set_defaults(func=self.log_tail)
|
|
50
|
+
|
|
51
|
+
sub_parser = commands.add_parser("download", help="Download log for task")
|
|
52
|
+
sub_parser.add_argument("source_app", metavar="source-app", help="Name of the Drove application that started the task")
|
|
53
|
+
sub_parser.add_argument("task_id", metavar="task-id", help="Task ID")
|
|
54
|
+
sub_parser.add_argument("tasklogfile", help="Log filename to download")
|
|
55
|
+
sub_parser.add_argument("--out", "-o", help="Filename to download to. Default is the same filename as provided.")
|
|
56
|
+
sub_parser.set_defaults(func=self.log_download)
|
|
57
|
+
|
|
58
|
+
super().populate_options(drove_client, parser)
|
|
59
|
+
|
|
60
|
+
def create_task(self, options: SimpleNamespace):
|
|
61
|
+
try:
|
|
62
|
+
with open(options.spec_file, 'r') as fp:
|
|
63
|
+
spec = json.load(fp)
|
|
64
|
+
operation = {
|
|
65
|
+
"type": "CREATE",
|
|
66
|
+
"spec": spec,
|
|
67
|
+
"opSpec": {
|
|
68
|
+
"timeout": "5m",
|
|
69
|
+
"parallelism": 1,
|
|
70
|
+
"failureStrategy": "STOP"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
data = self.drove_client.post("/apis/v1/tasks/operations", operation)
|
|
74
|
+
print("Task created. Source App Name: {sourceAppName} Task ID: {taskId}. Drove assigned task ID: {internalTaskId}"
|
|
75
|
+
.format(internalTaskId=data["taskId"], sourceAppName=spec["sourceAppName"], taskId=spec["taskId"]))
|
|
76
|
+
except (OSError, IOError) as e:
|
|
77
|
+
print("Error creating task. Error: " + str(e))
|
|
78
|
+
|
|
79
|
+
def kill_task(self, options: SimpleNamespace):
|
|
80
|
+
operation = {
|
|
81
|
+
"type": "KILL",
|
|
82
|
+
"sourceAppName" : options.source_app_name,
|
|
83
|
+
"taskId" : options.task_id,
|
|
84
|
+
"opSpec": {
|
|
85
|
+
"timeout": "5m",
|
|
86
|
+
"parallelism": 1,
|
|
87
|
+
"failureStrategy": "STOP"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
data = self.drove_client.post("/apis/v1/tasks/operations", operation)
|
|
91
|
+
print("Task kill issued")
|
|
92
|
+
|
|
93
|
+
def list_task(self, options: SimpleNamespace):
|
|
94
|
+
data = self.drove_client.get('/apis/v1/tasks')
|
|
95
|
+
task_rows = []
|
|
96
|
+
for task in data:
|
|
97
|
+
if options.app and task["sourceAppName"] != options.app:
|
|
98
|
+
continue
|
|
99
|
+
row = []
|
|
100
|
+
row.append(task["instanceId"])
|
|
101
|
+
row.append(task["sourceAppName"])
|
|
102
|
+
row.append(task["taskId"])
|
|
103
|
+
row.append(task["state"])
|
|
104
|
+
row.append(task.get("hostname", ""))
|
|
105
|
+
cpu_list = [r for r in task.get("resources", list()) if r.get("type", "") == "CPU"]
|
|
106
|
+
if len(cpu_list) > 0:
|
|
107
|
+
row.append(len(cpu_list[0].get("cores", dict())))
|
|
108
|
+
memory_list = [r for r in task.get("resources", list()) if r.get("type", "") == "MEMORY"]
|
|
109
|
+
if len(memory_list) > 0:
|
|
110
|
+
row.append(sum(memory_list[0].get("memoryInMB", dict()).values()))
|
|
111
|
+
row.append(droveutils.to_date(task["created"]))
|
|
112
|
+
row.append(droveutils.to_date(task["updated"]))
|
|
113
|
+
|
|
114
|
+
task_rows.append(row)
|
|
115
|
+
|
|
116
|
+
task_rows = sorted(task_rows, key=itemgetter(options.sort), reverse=options.reverse)
|
|
117
|
+
|
|
118
|
+
headers = ["Id", "Source App", "Task ID", "State", "Host", "CPU", "Memory(MB)", "Created", "Updated"]
|
|
119
|
+
droveutils.print_table(headers, task_rows)
|
|
120
|
+
|
|
121
|
+
def show_task(self, options: SimpleNamespace):
|
|
122
|
+
raw = self.drove_client.get("/apis/v1/tasks/{source_app}/instances/{task_id}".format(source_app = options.source_app, task_id = options.task_id))
|
|
123
|
+
data = OrderedDict()
|
|
124
|
+
data["Source App"] = raw["sourceAppName"]
|
|
125
|
+
data["Task ID"] = raw["taskId"]
|
|
126
|
+
data["Instance ID"] = raw["instanceId"]
|
|
127
|
+
data["State"] = raw["state"]
|
|
128
|
+
data["Executor Host"] = raw["hostname"]
|
|
129
|
+
droveutils.populate_resources(raw, data)
|
|
130
|
+
data["Executable"] = "{url} ({type})".format_map(raw["executable"])
|
|
131
|
+
data["Volumes"] = ", ".join(["{pathOnHost} (Mounted as {pathInContainer} in {mode} mode)".format_map(v) for v in raw["volumes"]])
|
|
132
|
+
data["Logging Mode"] = raw.get("logging", dict()).get("type", "")
|
|
133
|
+
data["Metadata"] = ", ".join(["%s: %s" % (key,value) for (key, value) in raw.get("metadata", dict())])
|
|
134
|
+
result = raw.get("taskResult", dict())
|
|
135
|
+
if len(result) > 0:
|
|
136
|
+
data["Result"] = "{status} (Task exit code: {exitCode})".format_map(result)
|
|
137
|
+
else:
|
|
138
|
+
data["Result"] = ""
|
|
139
|
+
data["Error Message"] = raw.get("errorMessage", "").strip('\n')
|
|
140
|
+
data["Created"] = droveutils.to_date(raw.get("created"))
|
|
141
|
+
data["Last Updated"] = droveutils.to_date(raw.get("updated"))
|
|
142
|
+
|
|
143
|
+
droveutils.print_dict(data)
|
|
144
|
+
|
|
145
|
+
def show_logs_list(self, options):
|
|
146
|
+
droveutils.list_logs(self.drove_client, "tasks", options.source_app, options.task_id)
|
|
147
|
+
|
|
148
|
+
def log_tail(self, options):
|
|
149
|
+
droveutils.tail_log(self.drove_client, "tasks", options.source_app, options.task_id, options.file)
|
|
150
|
+
|
|
151
|
+
def log_download(self, options):
|
|
152
|
+
filename = options.tasklogfile
|
|
153
|
+
if options.out and len(options.out) > 0:
|
|
154
|
+
filename = options.out
|
|
155
|
+
droveutils.download_log(self.drove_client, "tasks", options.source_app, options.task_id, options.tasklogfile, filename)
|