|
| 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