Skip to content

Commit 2ceaa39

Browse files
committed
cli: cloudmonkey the command line interface
cloudmonkey ----------- Apache CloudStack's very own monkey powered command line interface based on Marvin. The neglected robot and monkey should rule the world! Features: - it's a shell and also a terminal tool - scalable to find and run old and new APIs - intuitive grammar and verbs - autocompletion (functional hack) - shell execution using ! or shell - cfg support: user defined variables, like prompt, ruler, host, port etc. - history - colors - dynamic API loading and rule generation - leverages Marvin to get latest autogenerated APIs - emacs like shortcuts on prompt - uses apiKey and secretKey to interact with mgmt server - logs all client commands - PEP-8 compliant code TODOs: - Reverse search - Fix input and output processing Signed-off-by: Rohit Yadav <bhaisaab@apache.org>
1 parent ff9e609 commit 2ceaa39

2 files changed

Lines changed: 349 additions & 0 deletions

File tree

tools/cli/cloudmonkey/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# Use following rules for versioning:
19+
# <cli major version>.<cloudstack minor version>.<cloudstack major version>
20+
# Example: For CloudStack 4.1.x, CLI version should be 0.1.4
21+
__version__ = "0.0.4"
22+
23+
try:
24+
from cloudmonkey import *
25+
except ImportError, e:
26+
print e
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
try:
21+
import atexit
22+
import cmd
23+
import clint
24+
import codecs
25+
import logging
26+
import os
27+
import pdb
28+
import readline
29+
import rlcompleter
30+
import sys
31+
import types
32+
33+
from clint.textui import colored
34+
from ConfigParser import ConfigParser, SafeConfigParser
35+
36+
from marvin.cloudstackConnection import cloudConnection
37+
from marvin.cloudstackException import cloudstackAPIException
38+
from marvin.cloudstackAPI import *
39+
from marvin import cloudstackAPI
40+
except ImportError, e:
41+
print "Import error in %s : %s" % (__name__, e)
42+
import sys
43+
sys.exit()
44+
45+
log_fmt = '%(asctime)s - %(filename)s:%(lineno)s - [%(levelname)s] %(message)s'
46+
logger = logging.getLogger(__name__)
47+
completions = cloudstackAPI.__all__
48+
49+
50+
class CloudStackShell(cmd.Cmd):
51+
intro = "☁ Apache CloudStack CLI. Type help or ? to list commands.\n"
52+
ruler = "-"
53+
config_file = os.path.expanduser('~/.cloudmonkey_config')
54+
55+
def __init__(self):
56+
self.config_fields = {'host': 'localhost', 'port': '8080',
57+
'apiKey': '', 'secretKey': '',
58+
'prompt': '🙉 cloudmonkey> ', 'color': 'true',
59+
'log_file':
60+
os.path.expanduser('~/.cloudmonkey_log'),
61+
'history_file':
62+
os.path.expanduser('~/.cloudmonkey_history')}
63+
if os.path.exists(self.config_file):
64+
config = self.read_config()
65+
else:
66+
for key in self.config_fields.keys():
67+
setattr(self, key, self.config_fields[key])
68+
config = self.write_config()
69+
print "Set your api and secret keys using the set command!"
70+
71+
for key in self.config_fields.keys():
72+
setattr(self, key, config.get('CLI', key))
73+
74+
self.prompt += " " # Cosmetic fix for prompt
75+
logging.basicConfig(filename=self.log_file,
76+
level=logging.DEBUG, format=log_fmt)
77+
self.logger = logging.getLogger(self.__class__.__name__)
78+
79+
cmd.Cmd.__init__(self)
80+
# Update config if config_file does not exist
81+
if not os.path.exists(self.config_file):
82+
config = self.write_config()
83+
84+
# Fix autocompletion issue
85+
if sys.platform == "darwin":
86+
readline.parse_and_bind("bind ^I rl_complete")
87+
else:
88+
readline.parse_and_bind("tab: complete")
89+
90+
# Enable history support
91+
try:
92+
if os.path.exists(self.history_file):
93+
readline.read_history_file(self.history_file)
94+
atexit.register(readline.write_history_file, self.history_file)
95+
except IOError:
96+
print("Error: history support")
97+
98+
def read_config(self):
99+
config = ConfigParser()
100+
try:
101+
with open(self.config_file, 'r') as cfg:
102+
config.readfp(cfg)
103+
for section in config.sections():
104+
for option in config.options(section):
105+
logger.debug("[%s] %s=%s" % (section, option,
106+
config.get(section, option)))
107+
except IOError, e:
108+
self.print_shell("Error: config_file not found", e)
109+
return config
110+
111+
def write_config(self):
112+
config = ConfigParser()
113+
config.add_section('CLI')
114+
for key in self.config_fields.keys():
115+
config.set('CLI', key, getattr(self, key))
116+
with open(self.config_file, 'w') as cfg:
117+
config.write(cfg)
118+
return config
119+
120+
def emptyline(self):
121+
pass
122+
123+
def print_shell(self, *args):
124+
try:
125+
for arg in args:
126+
if isinstance(type(args), types.NoneType):
127+
continue
128+
if self.color == 'true':
129+
if str(arg).count(self.ruler) == len(str(arg)):
130+
print colored.green(arg),
131+
elif 'type' in arg:
132+
print colored.green(arg),
133+
elif 'state' in arg:
134+
print colored.yellow(arg),
135+
elif 'id =' in arg:
136+
print colored.cyan(arg),
137+
elif 'name =' in arg:
138+
print colored.magenta(arg),
139+
elif ':' in arg:
140+
print colored.blue(arg),
141+
elif 'Error' in arg:
142+
print colored.red(arg),
143+
else:
144+
print arg,
145+
else:
146+
print arg,
147+
print
148+
except Exception, e:
149+
print colored.red("Error: "), e
150+
151+
# FIXME: Fix result processing and printing
152+
def print_result(self, result, response, api_mod):
153+
def print_result_as_list():
154+
if result is None:
155+
return
156+
for node in result:
157+
print_result_as_instance(node)
158+
159+
def print_result_as_instance(node):
160+
for attribute in dir(response):
161+
if "__" not in attribute:
162+
attribute_value = getattr(node, attribute)
163+
if isinstance(attribute_value, list):
164+
self.print_shell("\n%s:" % attribute)
165+
try:
166+
self.print_result(attribute_value,
167+
getattr(api_mod, attribute)(),
168+
api_mod)
169+
except AttributeError, e:
170+
pass
171+
elif attribute_value is not None:
172+
self.print_shell("%s = %s" %
173+
(attribute, attribute_value))
174+
self.print_shell(self.ruler * 80)
175+
176+
if result is None:
177+
return
178+
179+
if type(result) is types.InstanceType:
180+
print_result_as_instance(result)
181+
elif isinstance(result, list):
182+
print_result_as_list()
183+
elif isinstance(result, str):
184+
print result
185+
elif isinstance(type(result), types.NoneType):
186+
print_result_as_instance(result)
187+
elif not (str(result) is None):
188+
self.print_shell(result)
189+
190+
def do_quit(self, s):
191+
"""
192+
Quit Apache CloudStack CLI
193+
"""
194+
self.print_shell("Bye!")
195+
return True
196+
197+
def do_shell(self, args):
198+
"""
199+
Execute shell commands using shell <command> or !<command>
200+
Example: !ls or shell ls
201+
"""
202+
os.system(args)
203+
204+
def make_request(self, command, requests={}):
205+
conn = cloudConnection(self.host, port=int(self.port),
206+
apiKey=self.apiKey, securityKey=self.secretKey,
207+
logging=logging.getLogger("cloudConnection"))
208+
try:
209+
response = conn.make_request(command, requests)
210+
except cloudstackAPIException, e:
211+
self.print_shell("API Error", e)
212+
return None
213+
return response
214+
215+
def default(self, args):
216+
args = args.split(" ")
217+
api_name = args[0]
218+
219+
try:
220+
api_cmd_str = "%sCmd" % api_name
221+
api_rsp_str = "%sResponse" % api_name
222+
api_mod = __import__("marvin.cloudstackAPI.%s" % api_name,
223+
globals(), locals(), [api_cmd_str], -1)
224+
api_cmd = getattr(api_mod, api_cmd_str)
225+
api_rsp = getattr(api_mod, api_rsp_str)
226+
except ImportError, e:
227+
self.print_shell("Error: API %s not found!" % e)
228+
return
229+
230+
command = api_cmd()
231+
response = api_rsp()
232+
#FIXME: Parsing logic
233+
args_dict = dict(map(lambda x: x.split("="),
234+
args[1:])[x] for x in range(len(args) - 1))
235+
236+
for attribute in dir(command):
237+
if attribute in args_dict:
238+
setattr(command, attribute, args_dict[attribute])
239+
240+
result = self.make_request(command, response)
241+
try:
242+
self.print_result(result, response, api_mod)
243+
except Exception as e:
244+
self.print_shell("🙈 Error on parsing and printing", e)
245+
246+
def completedefault(self, text, line, begidx, endidx):
247+
mline = line.partition(" ")[2]
248+
offs = len(mline) - len(text)
249+
return [s[offs:] for s in completions if s.startswith(mline)]
250+
251+
def do_api(self, args):
252+
"""
253+
Make raw api calls. Syntax: api <apiName> <args>=<values>. Example:
254+
api listAccount listall=true
255+
"""
256+
if len(args) > 0:
257+
return self.default(args)
258+
else:
259+
self.print_shell("Please use a valid syntax")
260+
261+
def complete_api(self, text, line, begidx, endidx):
262+
return self.completedefault(text, line, begidx, endidx)
263+
264+
def do_set(self, args):
265+
"""
266+
Set config for CloudStack CLI. Available options are:
267+
host, port, apiKey, secretKey, log_file, history_file
268+
"""
269+
args = args.split(' ')
270+
if len(args) == 2:
271+
key, value = args
272+
# Note: keys and fields should have same names
273+
setattr(self, key, value)
274+
self.write_config()
275+
else:
276+
self.print_shell("Please use the syntax: set valid-key value")
277+
278+
def complete_set(self, text, line, begidx, endidx):
279+
mline = line.partition(" ")[2]
280+
offs = len(mline) - len(text)
281+
return [s[offs:] for s in
282+
['host', 'port', 'apiKey', 'secretKey', 'prompt', 'color',
283+
'log_file', 'history_file'] if s.startswith(mline)]
284+
285+
286+
def main():
287+
grammar = ['list', 'create', 'delete', 'update', 'disable', 'enable',
288+
'add', 'remove']
289+
self = CloudStackShell
290+
for rule in grammar:
291+
setattr(self, 'completions_' + rule, map(lambda x: x.replace(rule, ''),
292+
filter(lambda x: rule in x,
293+
completions)))
294+
295+
def add_grammar(rule):
296+
def grammar_closure(self, args):
297+
self.default(rule + args)
298+
return grammar_closure
299+
300+
grammar_handler = add_grammar(rule)
301+
grammar_handler.__doc__ = "%ss resources" % rule.capitalize()
302+
grammar_handler.__name__ = 'do_' + rule
303+
setattr(self, grammar_handler.__name__, grammar_handler)
304+
305+
def add_completer(rule):
306+
def completer_closure(self, text, line, begidx, endidx):
307+
mline = line.partition(" ")[2]
308+
offs = len(mline) - len(text)
309+
return [s[offs:] for s in getattr(self, 'completions_' + rule)
310+
if s.startswith(mline)]
311+
return completer_closure
312+
313+
completion_handler = add_completer(rule)
314+
completion_handler.__name__ = 'complete_' + rule
315+
setattr(self, completion_handler.__name__, completion_handler)
316+
317+
if len(sys.argv) > 1:
318+
CloudStackShell().onecmd(' '.join(sys.argv[1:]))
319+
else:
320+
CloudStackShell().cmdloop()
321+
322+
if __name__ == "__main__":
323+
main()

0 commit comments

Comments
 (0)