Add cloud-tool into FOSS

This commit is contained in:
edison 2010-10-26 18:05:11 -07:00
parent d7bf859d92
commit 066d94f6f9
11 changed files with 659 additions and 2 deletions

View File

@ -210,8 +210,8 @@ authorizeNetworkGroupIngress=com.cloud.api.commands.AuthorizeNetworkGroupIngress
revokeNetworkGroupIngress=com.cloud.api.commands.RevokeNetworkGroupIngressCmd;11
listNetworkGroups=com.cloud.api.commands.ListNetworkGroupsCmd;11
registerPreallocatedLun=com.cloud.server.api.commands.RegisterPreallocatedLunCmd;1
deletePreallocatedLun=com.cloud.server.api.commands.DeletePreallocatedLunCmd;1
registerPreallocatedLun=com.cloud.api.commands.RegisterPreallocatedLunCmd;1
deletePreallocatedLun=com.cloud.api.commands.DeletePreallocatedLunCmd;1
listPreallocatedLuns=com.cloud.api.commands.ListPreallocatedLunsCmd;1
#### vm group commands

11
cloud-cli/bindir/cloud-tool Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env python
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import cloudtool
ret = cloudtool.main()
if ret: sys.exit(ret)

View File

@ -0,0 +1,25 @@
'''
Created on Aug 2, 2010
@author: rudd-o
'''
import os,pkgutil
def get_all_apis():
apis = []
for x in pkgutil.walk_packages([os.path.dirname(__file__)]):
loader = x[0].find_module(x[1])
try: module = loader.load_module("cloudapis." + x[1])
except ImportError: continue
apis.append(module)
return apis
def lookup_api(api_name):
api = None
matchingapi = [ x for x in get_all_apis() if api_name.replace("-","_") == x.__name__.split(".")[-1] ]
if not matchingapi: api = None
else: api = matchingapi[0]
if api: api = getattr(api,"implementor")
return api

View File

@ -0,0 +1,63 @@
'''Implements the Amazon API'''
import boto.ec2
import os
from cloudtool.utils import describe,OptionConflictError,OptionValueError
raise ImportError
class AmazonAPI:
@describe("access_key", "Amazon access key")
@describe("secret_key", "Amazon secret key")
@describe("region", "Amazon region")
@describe("endpoint", "Amazon endpoint")
def __init__(self,
access_key=os.environ.get("AWS_ACCESS_KEY_ID",None),
secret_key=os.environ.get("AWS_SECRET_ACCESS_KEY",None),
region=None,
endpoint=None):
if not access_key: raise OptionValueError,"you need to specify an access key"
if not secret_key: raise OptionValueError,"you need to specify a secret key"
if region and endpoint:
raise OptionConflictError,("mutually exclusive with --endpoint",'--region')
self.__dict__.update(locals())
def _get_regions(self):
return boto.ec2.regions(aws_access_key_id=self.access_key,aws_secret_access_key=self.secret_key)
def _get_region(self,name):
try: return [ x for x in self._get_regions() if x.name == name ][0]
except IndexError: raise KeyError,name
def _connect(self):
if self.region:
region = self._get_region(self.region)
self.connection = region.connect(
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key
)
else:
self.connection = boto.ec2.connection.EC2Connection(
host=self.endpoint,
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key
)
def list_regions(self):
"""Lists all regions"""
regions = self._get_regions()
for r in regions: print r
def get_all_images(self):
"""Lists all images"""
self._connect()
images = self.connection.get_all_images()
for i in images: print i
def get_region(self):
"""Gets the region you're connecting to"""
self._connect()
print self.connection.region
implementor = AmazonAPI

View File

@ -0,0 +1,113 @@
'''Implements the Cloud.com API'''
from cloudtool.utils import describe
import urllib
import urllib2
import os
import xml.dom.minidom
class CloudAPI:
@describe("server", "Management Server host name or address")
@describe("responseformat", "Response format: xml or json")
def __init__(self,
server="127.0.0.1:8096",
responseformat="xml",
):
self.__dict__.update(locals())
def _make_request(self,command,parameters=None):
'''Command is a string, parameters is a dictionary'''
if ":" in self.server:
host,port = self.server.split(":")
port = int(port)
else:
host = self.server
port = 8096
url = "http://" + self.server + "/?"
if not parameters: parameters = {}
parameters["command"] = command
parameters["response"] = self.responseformat
querystring = urllib.urlencode(parameters)
url += querystring
f = urllib2.urlopen(url)
data = f.read()
return data
def load_dynamic_methods():
'''creates smart function objects for every method in the commands.xml file'''
def getText(nodelist):
rc = []
for node in nodelist:
if node.nodeType == node.TEXT_NODE: rc.append(node.data)
return ''.join(rc)
# FIXME figure out installation and packaging
xmlfile = os.path.join("/etc/cloud/cli/","commands.xml")
dom = xml.dom.minidom.parse(xmlfile)
for cmd in dom.getElementsByTagName("command"):
name = getText(cmd.getElementsByTagName('name')[0].childNodes).strip()
assert name
description = cmd.getElementsByTagName('name')[0].getAttribute("description")
if description: description = '"""%s"""' % description
else: description = ''
arguments = []
options = []
descriptions = []
for param in cmd.getElementsByTagName('arg'):
argname = getText(param.childNodes).strip()
assert argname
required = param.getAttribute("required").strip()
if required == 'true': required = True
elif required == 'false': required = False
else: raise AssertionError, "Not reached"
if required: arguments.append(argname)
options.append(argname)
description = param.getAttribute("description").strip()
if description: descriptions.append( (argname,description) )
funcparams = ["self"] + [ "%s=None"%o for o in options ]
funcparams = ", ".join(funcparams)
code = """
def %s(%s):
%s
parms = locals()
del parms["self"]
for arg in %r:
if locals()[arg] is None:
raise TypeError, "%%s is a required option"%%arg
for k,v in parms.items():
if v is None: del parms[k]
output = self._make_request("%s",parms)
return output
"""%(name,funcparams,description,arguments,name)
namespace = {}
exec code.strip() in namespace
func = namespace[name]
for argname,description in descriptions:
func = describe(argname,description)(func)
yield (name,func)
for name,meth in load_dynamic_methods(): setattr(CloudAPI,name,meth)
implementor = CloudAPI
del name,meth,describe,load_dynamic_methods

View File

@ -0,0 +1,51 @@
'''
Created on Aug 2, 2010
@author: rudd-o
'''
import sys
import cloudapis as apis
import cloudtool.utils as utils
def main(argv=None):
if argv == None:
argv = sys.argv
prelim_args = [ x for x in argv[1:] if not x.startswith('-') ]
parser = utils.get_parser()
api = __import__("cloudapis")
apis = getattr(api, "implementor")
if len(prelim_args) == 0:
parser.error("you need to specify an API as the first argument\n\nSupported APIs:\n" + "\n".join(utils.get_api_list()))
elif len(prelim_args) == 1:
commandlist = utils.get_command_list(apis)
parser.error("you need to specify a command name as the second argument\n\nCommands supported by the %s API:\n"%prelim_args[0] + "\n".join(commandlist))
command = utils.lookup_command_in_api(apis,prelim_args[1])
if not command: parser.error("command %r not supported by the %s API"%(prelim_args[1],prelim_args[0]))
parser = utils.get_parser(apis.__init__,command)
argv = argv[1:]
opts,args,api_optionsdict,cmd_optionsdict = parser.parse_args(argv)
try:
api = apis(**api_optionsdict)
except utils.OptParseError,e:
parser.error(str(e))
command = utils.lookup_command_in_api(api,args[1])
# we now discard the first two arguments as those necessarily are the api and command names
args = args[2:]
try: return command(*args,**cmd_optionsdict)
except TypeError,e: parser.error(str(e))
if __name__ == '__main__':
main(argv)

View File

@ -0,0 +1,185 @@
'''
Created on Aug 2, 2010
@author: rudd-o
'''
import sys
import os
import inspect
from optparse import OptionParser, OptParseError, BadOptionError, OptionError, OptionConflictError, OptionValueError
import cloudapis as apis
def describe(name,desc):
def inner(decoratee):
if not hasattr(decoratee,"descriptions"): decoratee.descriptions = {}
decoratee.descriptions[name] = desc
return decoratee
return inner
def error(msg):
sys.stderr.write(msg)
sys.stderr.write("\n")
class MyOptionParser(OptionParser):
def error(self, msg):
error("%s: %s\n" % (self.get_prog_name(),msg))
self.print_usage(sys.stderr)
self.exit(os.EX_USAGE)
def parse_args(self,*args,**kwargs):
options,arguments = OptionParser.parse_args(self,*args,**kwargs)
def prune_options(options,alist):
"""Given 'options' -- a list of arguments to OptionParser.add_option,
and a set of optparse Values, return a dictionary of only those values
that apply exclusively to 'options'"""
return dict( [ (k,getattr(options,k)) for k in dir(options) if k in alist ] )
api_options = prune_options(options,self.api_dests)
cmd_options = prune_options(options,self.cmd_dests)
return options,arguments,api_options,cmd_options
def get_parser(api_callable=None,cmd_callable=None): # this should probably be the __init__ method of myoptionparser
def getdefaulttag(default):
if default is not None: return " [Default: %default]"
return ''
def get_arguments_and_options(callable):
"""Infers and returns arguments and options based on a callable's signature.
Cooperates with decorator @describe"""
try:
funcargs = inspect.getargspec(callable).args
defaults = inspect.getargspec(callable).defaults
except:
funcargs = inspect.getargspec(callable)[0]
defaults = inspect.getargspec(callable)[3]
if not defaults: defaults = []
args = funcargs[1:len(funcargs)-len(defaults)] # this assumes self, so assumes methods
opts = funcargs[len(funcargs)-len(defaults):]
try: descriptions = callable.descriptions
except AttributeError: descriptions = {}
arguments = [ (argname, descriptions.get(argname,'') ) for argname in args ]
options = [ [
("--%s"%argname.replace("_","-"),),
{
"dest":argname,
"help":descriptions.get(argname,'') + getdefaulttag(default),
"default":default,
}
] for argname,default in zip(opts,defaults) ]
return arguments,options
basic_usage = "usage: %prog [options...] "
api_name = "<api>"
cmd_name = "<command>"
description = "%prog is a command-line tool to access several cloud APIs."
arguments = ''
argexp = ""
if api_callable:
api_name = api_callable.__module__.split(".")[-1].replace("_","-")
api_arguments,api_options = get_arguments_and_options(api_callable)
assert len(api_arguments) is 0 # no mandatory arguments for class initializers
if cmd_callable:
cmd_name = cmd_callable.func_name.replace("_","-")
cmd_arguments,cmd_options = get_arguments_and_options(cmd_callable)
if cmd_arguments:
arguments = " " + " ".join( [ s[0].upper() for s in cmd_arguments ] )
argexp = "\n\nArguments:\n" + "\n".join ( " %s\n %s"%(s.upper(),u) for s,u in cmd_arguments )
description = cmd_callable.__doc__
api_command = "%s %s"%(api_name,cmd_name)
if description: description = "\n\n" + description
else: description = ''
usage = basic_usage + api_command + arguments + description + argexp
parser = MyOptionParser(usage=usage, add_help_option=False)
parser.add_option('--help', action="help")
group = parser.add_option_group("General options")
group.add_option('-v', '--verbose', dest="verbose", help="Print extra output")
# now we need to derive the short options. we initialize the known fixed longopts and shortopts
longopts = [ '--verbose' ]
# we add the known long options, sorted and grouped to guarantee a stable short opt set regardless of order
if api_callable and api_options:
longopts += sorted([ x[0][0] for x in api_options ])
if cmd_callable and cmd_options:
longopts += sorted([ x[0][0] for x in cmd_options ])
# we use this function to derive a suitable short option and remember the already-used short options
def derive_shortopt(longopt,usedopts):
"""longopt begins with a dash"""
shortopt = None
for x in xrange(2,10000):
try: shortopt = "-" + longopt[x]
except IndexError:
shortopt = None
break
if shortopt in usedopts: continue
usedopts.append(shortopt)
break
return shortopt
# now we loop through the long options and assign a suitable short option, saving the short option for later use
long_to_short = {}
alreadyusedshorts = []
for longopt in longopts:
long_to_short[longopt] = derive_shortopt(longopt,alreadyusedshorts)
parser.api_dests = []
if api_callable and api_options:
group = parser.add_option_group("Options for the %s API"%api_name)
for a in api_options:
shortopt = long_to_short[a[0][0]]
if shortopt: group.add_option(shortopt,a[0][0],**a[1])
else: group.add_option(a[0][0],**a[1])
parser.api_dests.append(a[1]["dest"])
parser.cmd_dests = []
if cmd_callable and cmd_options:
group = parser.add_option_group("Options for the %s command"%cmd_name)
for a in cmd_options:
shortopt = long_to_short[a[0][0]]
if shortopt: group.add_option(shortopt,a[0][0],**a[1])
else: group.add_option(a[0][0],**a[1])
parser.cmd_dests.append(a[1]["dest"])
return parser
def lookup_command_in_api(api,command_name):
command = getattr(api,command_name.replace("-","_"),None)
return command
def get_api_list():
apilist = []
for api in apis.get_all_apis():
api_module = api
api_name = api.__name__.split(".")[-1]
if not api_name.startswith("_") and hasattr(api_module,'__doc__'):
apilist.append( " %20s %s"%(api_name.replace("_",'-'),api_module.__doc__) )
return apilist
def get_command_list(api):
cmds = []
for cmd_name in dir(api):
cmd = getattr(api,cmd_name)
if callable(cmd) and not cmd_name.startswith("_"):
if cmd.__doc__: docstring = cmd.__doc__
else: docstring = ''
cmds.append( " %s %s"%(cmd_name.replace('_','-'),docstring) )
return cmds

View File

@ -274,6 +274,13 @@ Group: System Environment/Libraries
The Cloud.com console proxy is the service in charge of granting console
access into virtual machines managed by the Cloud.com CloudStack.
%package cli
Summary: Cloud.com command line tools
Requires: python
Group: System Environment/Libraries
%description cli
The Cloud.com command line tools contain a few Python modules that can call cloudStack APIs.
%if %{_premium}
@ -619,6 +626,13 @@ fi
%attr(0755,root,root) %{_bindir}/%{name}-setup-console-proxy
%dir %attr(770,root,root) %{_localstatedir}/log/%{name}/console-proxy
%files cli
%{_bindir}/%{name}-tool
%{_sysconfdir}/%{name}/cli/commands.xml
%dir %{_prefix}/lib*/python*/site-packages/%{name}tool
%{_prefix}/lib*/python*/site-packages/%{name}tool/*
%{_prefix}/lib*/python*/site-packages/%{name}apis.py
%if %{_premium}
%files test

View File

@ -0,0 +1,166 @@
/**
* Copyright (C) 2010 Cloud.com, Inc. All rights reserved.
*
* This software is licensed under the GNU General Public License v3 or later.
*
* It is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.cloud.utils.commandlinetool;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import com.cloud.utils.Pair;
public class BuildCommandLineInputFile {
private static Properties api_commands = new Properties();
private static String dirName="";
public static void main (String[] args) {
Properties preProcessedCommands = new Properties();
Class clas = null;
Enumeration e = null;
String[] fileNames = null;
//load properties
List<String> argsList = Arrays.asList(args);
Iterator<String> iter = argsList.iterator();
while (iter.hasNext()) {
String arg = iter.next();
// populate the file names
if (arg.equals("-f")) {
fileNames = iter.next().split(",");
}
if (arg.equals("-d")) {
dirName = iter.next();
}
}
if ((fileNames == null) || (fileNames.length == 0)){
System.out.println("Please specify input file(s) separated by coma using -f option");
System.exit(2);
}
for (String fileName : fileNames) {
try {
FileInputStream in = new FileInputStream(fileName);
preProcessedCommands.load(in);
}catch (FileNotFoundException ex) {
System.out.println("Can't find file " + fileName);
System.exit(2);
} catch (IOException ex1) {
System.out.println("Error reading from file " + ex1);
System.exit(2);
}
}
for (Object key : preProcessedCommands.keySet()) {
String preProcessedCommand = preProcessedCommands.getProperty((String)key);
String[] commandParts = preProcessedCommand.split(";");
api_commands.put(key, commandParts[0]);
}
e = api_commands.propertyNames();
try {
DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document doc = docBuilder.newDocument();
Element root = doc.createElement("commands");
doc.appendChild(root);
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
try {
clas = Class.forName(api_commands.getProperty(key));
Element child1 = doc.createElement("command");
root.appendChild(child1);
Element child2 = doc.createElement("name");
child1.appendChild(child2);
Text text = doc.createTextNode(key);
child2.appendChild(text);
Field m[] = clas.getDeclaredFields();
for (int i = 0; i < m.length; i++) {
if (m[i].getName().endsWith("s_properties")) {
m[i].setAccessible(true);
List<Pair<Enum, Boolean>> properties = (List<Pair<Enum, Boolean>>) m[i].get(null);
for (Pair property : properties){
if (!property.first().toString().equals("ACCOUNT_OBJ") && !property.first().toString().equals("USER_ID")){
Element child3 = doc.createElement("arg");
child1.appendChild(child3);
Class clas2 = property.first().getClass();
Method m2 = clas2.getMethod("getName");
text = doc.createTextNode(m2.invoke(property.first()).toString());
child3.appendChild(text);
child3.setAttribute("required", property.second().toString());
}
}
}
}
} catch (ClassNotFoundException ex2) {
System.out.println("Can't find class " + api_commands.getProperty(key));
System.exit(2);
}
}
TransformerFactory transfac = TransformerFactory.newInstance();
Transformer trans = transfac.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
trans.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter sw = new StringWriter();
StreamResult result = new StreamResult(sw);
DOMSource source = new DOMSource(doc);
trans.transform(source, result);
String xmlString = sw.toString();
//write xml to file
File f=new File(dirName + "/commands.xml");
Writer output = new BufferedWriter(new FileWriter(f));
output.write(xmlString);
output.close();
} catch (Exception ex) {
System.out.println(ex);
System.exit(2);
}
}
}

View File

@ -412,6 +412,31 @@ for vendor in _glob(_join("vendor","*")) + _glob(_join("cloudstack-proprietary",
# ====================== End vendor-specific plugins ====================
def generate_xml_api_description(task):
relationship = Utils.relpath(sourcedir,os.getcwd())
cp = [ _join(relationship,x) for x in task.generator.env.CLASSPATH.split(pathsep) ]
buildproducts = [ x.bldpath(task.env) for x in task.inputs ]
jars = [ x for x in buildproducts if x.endswith("jar") ]
properties = [ x for x in buildproducts if x.endswith("properties") ]
cp += jars
cp = pathsep.join(cp)
arguments = ["-f",",".join(properties),"-d",builddir]
ret = Utils.exec_command(["java","-cp",cp,"com.cloud.utils.commandlinetool.BuildCommandLineInputFile"]+arguments,log=True)
return ret
props = " client/tomcatconf/commands.properties"
jarnames = ['utils','server','core', 'api']
tgen = bld(
rule = generate_xml_api_description,
source = " ".join( [ 'target/jar/cloud-%s.jar'%x for x in jarnames ] ) + props,
target = 'commands.xml',
name = 'xmlapi',
after = 'runant',
install_path="${CLIDIR}"
)
#bld.process_after(tgen)
bld.install_files("${PYTHONDIR}/cloudtool", 'cloud-cli/cloudtool/*')
bld.install_as("${PYTHONDIR}/cloudapis.py", 'cloud-cli/cloudapis/cloud.py')
# ====================== Magic! =========================================

View File

@ -273,6 +273,8 @@ conf.env.CPLIBDIR = Utils.subst_vars(_join("${LIBDIR}","${CPPATH}"),conf.env)
conf.env.CPSYSCONFDIR = Utils.subst_vars(_join("${SYSCONFDIR}","${CPPATH}"),conf.env)
conf.env.CPLOGDIR = Utils.subst_vars(_join("${LOCALSTATEDIR}","log","${CPPATH}"),conf.env)
conf.env.CLIPATH = _join(conf.env.PACKAGE,"cli")
conf.env.IPALLOCATORLIBDIR = Utils.subst_vars(_join("${LIBDIR}","${IPALLOCATORPATH}"),conf.env)
conf.env.IPALLOCATORSYSCONFDIR = Utils.subst_vars(_join("${SYSCONFDIR}","${IPALLOCATORPATH}"),conf.env)
conf.env.IPALLOCATORLOGDIR = Utils.subst_vars(_join("${LOCALSTATEDIR}","log","${IPALLOCATORPATH}"),conf.env)
@ -286,6 +288,8 @@ conf.env.IPALOCATORLOG = _join(conf.env.IPALLOCATORLOGDIR,"ipallocator.log")
conf.env.SETUPDATADIR = Utils.subst_vars(_join("${DATADIR}","${SETUPPATH}"),conf.env)
conf.env.CLIDIR = Utils.subst_vars(_join("${SYSCONFDIR}","${CLIPATH}"),conf.env)
conf.env.SERVERSYSCONFDIR = Utils.subst_vars(_join("${SYSCONFDIR}","${SERVERPATH}"),conf.env)
if conf.env.DISTRO in ["Windows"]: