From fa41bc769c442287076d28a667a2f3967bae4ed1 Mon Sep 17 00:00:00 2001 From: Remi Bergsma Date: Sun, 17 Jan 2016 22:06:50 +0100 Subject: [PATCH] Revert "Merge pull request #1228 from borisroman/CLOUDSTACK-9149" This reverts commit c459dfe62c57c72ced99ddb0c7d8a9547a380305, reversing changes made to 2afb739f0656d14e5c298bf469f797fff6da221b. --- cloud-cli/bindir/cloud-tool | 28 ++ cloud-cli/bindir/cloudvoladm | 607 ++++++++++++++++++++++++++++++++ cloud-cli/cloudapis/__init__.py | 43 +++ cloud-cli/cloudapis/cloud.py | 198 +++++++++++ cloud-cli/cloudtool/__init__.py | 71 ++++ cloud-cli/cloudtool/utils.py | 169 +++++++++ 6 files changed, 1116 insertions(+) create mode 100755 cloud-cli/bindir/cloud-tool create mode 100755 cloud-cli/bindir/cloudvoladm create mode 100644 cloud-cli/cloudapis/__init__.py create mode 100644 cloud-cli/cloudapis/cloud.py create mode 100644 cloud-cli/cloudtool/__init__.py create mode 100644 cloud-cli/cloudtool/utils.py diff --git a/cloud-cli/bindir/cloud-tool b/cloud-cli/bindir/cloud-tool new file mode 100755 index 00000000000..4fcc8342228 --- /dev/null +++ b/cloud-cli/bindir/cloud-tool @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import sys +import os + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +import cloudtool + +ret = cloudtool.main() +if ret: sys.exit(ret) diff --git a/cloud-cli/bindir/cloudvoladm b/cloud-cli/bindir/cloudvoladm new file mode 100755 index 00000000000..16f31bde89f --- /dev/null +++ b/cloud-cli/bindir/cloudvoladm @@ -0,0 +1,607 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import sys +import os +import subprocess +import cloudtool +import urllib2 +from optparse import OptionParser, OptionGroup, OptParseError, BadOptionError, OptionError, OptionConflictError, OptionValueError +import xml.dom.minidom + +NetAppServerIP=None +NetAppUserName=None +NetAppPassword=None +CloudStackSvrIP=None +CloudStackSvrPort=8096 + + +cmds=["createvol","deletevol", "listvol", "createlun", "listlun", "destroylun", "assoclun", "disassoclun", "createpool", "modifypool", "destroypool", "listpools"] +header = "Volume Manager CLI, the available COMMANDS are:" + + +def cmd_help(): + print header + print + print "createpool add a new pool to the system" + print "modifypool change the allocation algorithm for a pool" + print "destroypool destroy a pool" + print "listpools list all the pools" + print "createvol add volume to a storage server" + print "deletevol delete volume on a storage server" + print "listvol list volume on a storage server" + print "createlun create LUN on a storage server" + print "listlun list LUN on a storage server" + print "destroylun destroy LUN on a storage server" + print "assoclun assoc LUN on a storage server" + print "disassoclun disassoc LUN on a storage server" + print + print "\"cloudvoladm COMMAND --help\" for more information on a specific command" + print + print "Global Options:" + print "--cloudStackMgtSvrIP the IP address of CloudStack Management Server" + print + print "Config file is ~/.cloudvoladmrc, Config options including: " + print "cloudStackMgtSvrIP=Cloudstack Management Server Address, which can be overriden by --cloudStackMgtSvrIP. If neither is provided, localhost is used." + +usage="Volume Manager CLI: add a new volume to a storage pool" +addvolParser= OptionParser(usage) +addvolParser.add_option("-i", metavar="server ip", dest="server_ip", help="The IP address of the storage server") +addvolParser.add_option("-u", metavar="username", dest="username", help="username to access the storage server with") +addvolParser.add_option("-w", metavar="password", dest="password", help="the password to access the storage server with") +addvolParser.add_option("-p", dest="pool_name", help="the name of the pool to allocate from") +addvolParser.add_option("-a", dest="aggregate_name", help="the name of aggregate") +addvolParser.add_option("-v", dest="vol_name", help="the name of volume") +addvolParser.add_option("-s", dest="size", help="size in GB eg.1") +optionalGroup = OptionGroup(addvolParser, "Optional") +optionalGroup.add_option("-r", dest="percentage", help="Percentage used for snapshot reserve") +optionalGroup.add_option("-S", dest="snapshots", help="Snapshot schedule in @ @ e.g. \"2 4 5@1,4 6@2,5\"") +addvolParser.add_option_group(optionalGroup) + +usage="Volume Manager CLI: remove a volume from a pool" +delvolParser= OptionParser(usage) +delvolParser.add_option("-i", metavar="server ip", dest="server_ip", help="The IP address of the storage server") +delvolParser.add_option("-a", dest="aggregate_name", help="The name of aggregate") +delvolParser.add_option("-v", dest="vol_name", help="The name of volume") + +usage="Volume Manager CLI: list all volumes known to exist in a pool" +listvolParser= OptionParser(usage) +listvolParser.add_option("-p", dest="pool_name", help="The name of the pool to list volumes from") + +usage="Volume Manager CLI: create a LUN on a pool" +createlunParser = OptionParser(usage) +createlunParser.add_option("-p", dest="pool_name", help="The name of the pool to add the volume to") +createlunParser.add_option("-s", dest="size", help="The size in GB e.g. 100") + +usage="Volume Manager CLI: list LUN on a pool" +listlunParser = OptionParser(usage) +listlunParser.add_option("-p", dest="pool_name", help="The pool name") + +usage="Volume Manager CLI: destroy a LUN " +destroylunParser = OptionParser(usage) +destroylunParser.add_option("-l", dest="lun_name", help="The LUN name") + +usage="Volume Manager CLI: Add a new pool to the system" +createPoolParser = OptionParser(usage) +createPoolParser.add_option("-p", dest="pool_name", help="The pool name") +createPoolParser.add_option("-A", dest="algorithm", help="roundrobin or leastfull") + + +usage="Volume Manager CLI: change the allocation algorithm for a pool" +modifyPoolParser = OptionParser(usage) +modifyPoolParser.add_option("-p", dest="pool_name", help="The pool name") +modifyPoolParser.add_option("-A", dest="algorithm", help="roundrobin or leastfull") + +usage="Volume Manager CLI: destroy a pool" +destroyPoolParser = OptionParser(usage) +destroyPoolParser.add_option("-p", dest="pool_name", help="The pool name") + +usage="Volume Manager CLI: list pools" +listPoolParser = OptionParser(usage) + +usage="Volume Manager CLI: associate a LUN with a guest that uses the stated IQN as client" +assocLunParser = OptionParser(usage) +assocLunParser.add_option("-g", dest="guest_iqn", help="the guest IQN. By default, it reads from /etc/iscsi/initiatorname.iscsi") +assocLunParser.add_option("-l", dest="lun_name", help="The LUN name") + +usage="Volume Manager CLI: disassociate a LUN with a guest that uses the stated IQN as client" +disassocLunParser = OptionParser(usage) +disassocLunParser.add_option("-g", dest="guest_iqn", help="the guest IQN. By default, it reads from /etc/iscsi/initiatorname.iscsi") +disassocLunParser.add_option("-l", dest="lun_name", help="The LUN name") + +cmdParsers = {cmds[0]:addvolParser, cmds[1]:delvolParser, cmds[2]:listvolParser, cmds[3]:createlunParser, cmds[4]:listlunParser, + cmds[5]:destroylunParser, cmds[6]:assocLunParser, cmds[7]:disassocLunParser, cmds[8]:createPoolParser, cmds[9]:modifyPoolParser, cmds[10]:destroyPoolParser, cmds[11]:listPoolParser} + + +def validate_parameter(input, signature): + (options, args) = signature.parse_args([]) + inputDict = input.__dict__ + sigDict = options.__dict__ + for k,v in sigDict.iteritems(): + inputValue = inputDict[k] + if inputValue == None: + print "Volume Manager CLI: missing operand " + print + signature.parse_args(["--help"]) + +def help_callback(option, opt, value, parser): + argv = sys.argv[1:] + try: + argv.remove(opt) + except: + argv.remove("--h") + + if len(argv) == 0: + cmd_help() + return + (options, args) = parser.parse_args(argv) + for cmd in cmds: + if cmd == args[0]: + cmdParsers[cmd].parse_args(["--help"]) + +def Help(): + usage = "usage: %prog cmd[createpool|listpools|modifypool|destroypool|createvol|deletevol|listvol|createlun|listlun|destroylun|assoclun|disassoclun] arg1 arg2 [--help, -h]" + parser = OptionParser(usage=usage, add_help_option=False) + parser.add_option("-h", "--help", action="callback", callback=help_callback); + parser.add_option("-i", metavar="server ip", dest="server_ip", help="The IP address of the storage server") + parser.add_option("--cloudstackSvr", dest="cloudstackSvr", help="cloudStack Server IP") + parser.add_option("-u", metavar="username", dest="username", help="username to access the storage server with") + parser.add_option("-w", metavar="password", dest="password", help="the password to access the storage server with") + parser.add_option("-p", dest="pool_name", help="the name of the pool to allocate from") + parser.add_option("-v", dest="vol_name", help="the name of volume") + parser.add_option("-A", dest="algorithm", help="roundrobin or leastfull") + parser.add_option("-a", dest="aggregate_name", help="The name of aggregate") + parser.add_option("-o", dest="options", help="requested option string for the NFS export or attach") + parser.add_option("-S", dest="snapshots", help="Snapshot schedule e.g.2 4 5@1,4 6@2,5") + parser.add_option("-r", dest="percentage", help="Percentage used for snapshot reservation") + parser.add_option("-s", dest="size", help="size in GB eg.1") + parser.add_option("-t", dest="target_iqn", help="the target IQN") + parser.add_option("-g", dest="guest_iqn", help="the guest IQN") + parser.add_option("-l", dest="lun_name", help="the LUN name") + + return parser + +def httpErrorHandler(code, msg): + try: + errtext = xml.dom.minidom.parseString(msg) + if errtext.getElementsByTagName("errortext") is not None: + err = getText(errtext.getElementsByTagName("errortext")[0].childNodes).strip() + print err + except: + print "Internal Error %s"%msg + +def getText(nodelist): + rc = [] + for node in nodelist: + if node.nodeType == node.TEXT_NODE: rc.append(node.data) + return ''.join(rc) + +def createvol(options): + args = [] + if options.pool_name == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + if options.aggregate_name == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + if options.vol_name == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + + if options.snapshots != None: + args += ['--snapshotpolicy=' + options.snapshots] + + if options.size == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + + if options.percentage != None: + args += ['--snapshotreservation=' + options.percentage] + + if NetAppServerIP == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + + if NetAppUserName == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + + if NetAppPassword == None: + print "Volume Manager CLI: missing operand " + print + addvolParser.parse_args(["--help"]) + + ''' + snapshot = options.snapshots + tokens = snapshot.split(" ") + print tokens + pos = 0; + for token in tokens: + if pos == 0: + #week + try: + week = int(token) + if week < 0: + raise + except: + print "Pls input correct week" + sys.exit(1) + elif pos == 1: + try: + day = int(token) + if day < 0: + raise + except: + print "Pls input correct day" + sys.exit(1) + + elif pos == 2: + try: + hours = token.split("@") + if int(hours[0]) < 0: + raise + hourlists = hours[1].split(",") + for hour in hourlists: + if int(hour) < 0 or int(hour) > 24: + raise + except: + print "Pls input correct hour" + sys.exit(1) + elif pos == 3: + try: + minutes = token.split("@") + if int(minutes[0]) < 0: + raise + + minuteslist = minutes[1].split(",") + for minute in minuteslist: + if int(minute) < 0 or int(minute) > 60: + raise + except: + print "Pls input correct hour" + sys.exit(1) + + ''' + + + try: + output = cloudtool.main(['cloud-tool', 'createVolumeOnFiler', '--ipaddress=' + NetAppServerIP , '--aggregatename=' + options.aggregate_name, + '--poolname=' + options.pool_name, '--volumename=' + options.vol_name, + '--size=' + options.size, + '--username=' + NetAppUserName, '--password=' + NetAppPassword, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"] + args) + print "Successfully added volume" + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing createvol cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing createvol cmd failed: %s" % (err.reason) + sys.exit(1) + + +def deletevol(options): + validate_parameter(options, delvolParser) + + try: + output = cloudtool.main(['cloud-tool', 'destroyVolumeOnFiler', '--ipaddress=' + NetAppServerIP, '--aggregatename=' + options.aggregate_name, + '--volumename=' + options.vol_name, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully deleted volume" + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing deletevol cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing deletevol cmd failed: %s" % (err.reason) + sys.exit(1) + +def listvol(options): + validate_parameter(options, listvolParser) + + try: + output = cloudtool.main(['cloud-tool', 'listVolumesOnFiler', '--poolname=' + options.pool_name, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]).strip("\n") + + xmlResult = xml.dom.minidom.parseString(output) + print "%-10s %-20s %-20s %-40s %-20s %-30s "%('Id', 'Address', 'Aggregate', 'Volume', 'Size(GB)', 'snapshotPolicy', ) + for volume in xmlResult.getElementsByTagName("volume"): + aggregatename = getText(volume.getElementsByTagName('aggregatename')[0].childNodes).strip() + id = getText(volume.getElementsByTagName('id')[0].childNodes).strip() + volumeName = getText(volume.getElementsByTagName('volumename')[0].childNodes).strip() + snapshotPolicy = getText(volume.getElementsByTagName('snapshotpolicy')[0].childNodes).strip() + ipaddress = getText(volume.getElementsByTagName('ipaddress')[0].childNodes).strip() + volSize = getText(volume.getElementsByTagName('size')[0].childNodes).strip() + print "%-10s %-20s %-20s %-40s %-20s %-30s "%(id, ipaddress, aggregatename, volumeName, volSize, snapshotPolicy) + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing listvol cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing listvol cmd failed: %s" % (err.reason) + sys.exit(1) + + +def createlun(options): + validate_parameter(options, createlunParser) + + try: + output = cloudtool.main(['cloud-tool', 'createLunOnFiler', '--name=' + options.pool_name, + '--size=' + options.size, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + + xmlResult = xml.dom.minidom.parseString(output.strip("\n")) + path = getText(xmlResult.getElementsByTagName("path")[0].childNodes).strip() + iqn = getText(xmlResult.getElementsByTagName("iqn")[0].childNodes).strip() + ipAddr = getText(xmlResult.getElementsByTagName('ipaddress')[0].childNodes).strip() + print "%-30s %-30s %-50s "%('LUN Name', 'Address', 'Target IQN') + print "%-30s %-30s %-50s "%(path, ipAddr, iqn) + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing createlun cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing createlun cmd failed: %s" % (err.reason) + sys.exit(1) + +def listlun(options): + validate_parameter(options, listlunParser) + + args = ["--poolname=" + options.pool_name, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"] + try: + output = cloudtool.main(['cloud-tool', 'listLunsOnFiler'] + args).strip("\n") + xmlResult = xml.dom.minidom.parseString(output) + + print "%-10s %-10s %-50s %-30s "%('LUN Id', 'Volume Id', 'Target IQN', 'LUN Name') + for volume in xmlResult.getElementsByTagName("lun"): + uuid = getText(volume.getElementsByTagName('id')[0].childNodes).strip() + path = getText(volume.getElementsByTagName('name')[0].childNodes).strip() + targetiqn = getText(volume.getElementsByTagName('iqn')[0].childNodes).strip() + volumeId = getText(volume.getElementsByTagName('volumeid')[0].childNodes).strip() + print "%-10s %-10s %-50s %-30s "%(uuid, volumeId, targetiqn, path) + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing listlun cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing listlun cmd failed: %s" % (err.reason) + sys.exit(1) + +def destroylun(options): + validate_parameter(options, destroylunParser) + + try: + output = cloudtool.main(['cloud-tool', 'destroyLunOnFiler', '--path=' + options.lun_name, + "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully destroyed LUN" + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing destroylun cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing destroylun failed: %s" % (err.reason) + sys.exit(1) + +def assoclun(options): + validate_parameter(options, assocLunParser) + + try: + output = cloudtool.main(['cloud-tool', 'associateLun', '--name=' + options.lun_name, + '--iqn=' + options.guest_iqn, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + xmlResult = xml.dom.minidom.parseString(output.strip("\n")) + lunid = getText(xmlResult.getElementsByTagName("id")[0].childNodes).strip() + iqn = getText(xmlResult.getElementsByTagName("targetiqn")[0].childNodes).strip() + ipAddr = getText(xmlResult.getElementsByTagName('ipaddress')[0].childNodes).strip() + print "%-30s %-30s %-50s "%('LUN Id', 'Address', 'Target IQN') + print "%-30s %-30s %-50s" % (lunid, ipAddr, iqn) + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing assoclun cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing assoclun failed: %s" % (err.reason) + sys.exit(1) + +def disassoclun(options): + validate_parameter(options, disassocLunParser) + + try: + output = cloudtool.main(['cloud-tool', 'dissociateLun', '--path=' + options.lun_name, + '--iqn=' + options.guest_iqn, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully dissociated LUN" + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing disassoclun cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing disassoclun failed: %s" % (err.reason) + sys.exit(1) + +def createpool(options): + validate_parameter(options, createPoolParser) + + if not (options.algorithm == "roundrobin" or options.algorithm == "leastfull"): + print "Only roundrobin or leastfull algorithm is supported" + sys.exit(1) + try: + output = cloudtool.main(['cloud-tool', 'createPool', '--name=' + options.pool_name, + '--algorithm=' + options.algorithm, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully created pool" + except urllib2.HTTPError, err: + code = err.code + print "executing createpool cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, err.read()) + sys.exit(1) + except urllib2.URLError, err: + print "executing createpool failed: %s" % (err.reason) + sys.exit(1) + +def listpools(options): + try: + output = cloudtool.main(['cloud-tool', 'listPools', + "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + output = output.strip("\n") + xmlResult = xml.dom.minidom.parseString(output) + print "%-10s %-40s %-10s" %('Id', 'Pool Name', 'Algorithm') + for volume in xmlResult.getElementsByTagName("pool"): + id = getText(volume.getElementsByTagName('id')[0].childNodes).strip() + poolname = getText(volume.getElementsByTagName('name')[0].childNodes).strip() + alg = getText(volume.getElementsByTagName('algorithm')[0].childNodes).strip() + print "%-10s %-40s %-10s"%(id, poolname, alg) + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing listpools cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing listpools failed, due to: %s" % (err.reason) + sys.exit(1) + +def modifypool(options): + validate_parameter(options, modifyPoolParser) + + try: + output = cloudtool.main(['cloud-tool', 'modifyPool', '--poolname=' + options.pool_name, + '--algorithm=' + options.algorithm, "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully modified pool" + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing modifypool cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing modifypool failed, due to: %s" % (err.reason) + sys.exit(1) + +def destroypool(options): + validate_parameter(options, destroyPoolParser) + + try: + output = cloudtool.main(['cloud-tool', 'deletePool', '--poolname=' + options.pool_name, + "--server=" + CloudStackSvrIP + ":" + str(CloudStackSvrPort), "--stripxml=false"]) + print "Successfully destroyed pool: " + options.pool_name + except urllib2.HTTPError, err: + code = err.code + msg = err.read() + print "executing destroypool cmd failed, http returning error code: %s" % (code) + httpErrorHandler(code, msg) + sys.exit(1) + except urllib2.URLError, err: + print "executing destroypool failed, due to: %s" % (err.reason) + sys.exit(1) + +def loadCfgFile(): + options = dict() + try: + cfgFile = open(os.environ['HOME'] + "/.cloudvoladmrc") + for line in cfgFile: + option = line.split("=") + if option[0] == "cloudStackMgtSvrIP": + options["cloudStackMgtSvrIP"] = option[1].strip("\n") + + except: + return None + + return options + +def getGuestIQN(): + try: + initialFile = open("/etc/iscsi/initiatorname.iscsi") + for line in initialFile: + iqn = line.split("=") + if iqn[0] == "InitiatorName": + return iqn[1].strip("\n") + except: + return None + return None + +if __name__ == '__main__': + parser = Help() + (options, args) = parser.parse_args() + + globalCfg = loadCfgFile() + + NetAppServerIP= options.server_ip + + NetAppUserName = options.username + + NetAppPassword = options.password + + CloudStackSvrIP = options.cloudstackSvr + if CloudStackSvrIP == None: + if globalCfg != None and "cloudStackMgtSvrIP" in globalCfg: + CloudStackSvrIP = globalCfg["cloudStackMgtSvrIP"] + if CloudStackSvrIP == None: + CloudStackSvrIP = "127.0.0.1" + + if options.guest_iqn == None: + GuestIQN = getGuestIQN() + options.__dict__["guest_iqn"] = GuestIQN + + if len(args) == 0: + sys.exit(1) + cmd = args[0] + if cmd == "createvol": + createvol(options) + elif cmd == "deletevol": + deletevol(options) + elif cmd == "listvol": + listvol(options) + elif cmd == "createlun": + createlun(options) + elif cmd == "listlun": + listlun(options) + elif cmd == "destroylun": + destroylun(options) + elif cmd == "assoclun": + assoclun(options) + elif cmd == "disassoclun": + disassoclun(options) + elif cmd == "createpool": + createpool(options) + elif cmd == "modifypool": + modifypool(options) + elif cmd == "destroypool": + destroypool(options) + elif cmd == "listpools": + listpools(options) + else: + print "Unrecoginzied command" + cmd_help() + sys.exit(1) diff --git a/cloud-cli/cloudapis/__init__.py b/cloud-cli/cloudapis/__init__.py new file mode 100644 index 00000000000..f23b2ce2fbf --- /dev/null +++ b/cloud-cli/cloudapis/__init__.py @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + + +''' +Created on Aug 2, 2010 + +''' + +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 + diff --git a/cloud-cli/cloudapis/cloud.py b/cloud-cli/cloudapis/cloud.py new file mode 100644 index 00000000000..a0e88805c82 --- /dev/null +++ b/cloud-cli/cloudapis/cloud.py @@ -0,0 +1,198 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + + +'''Implements the CloudStack API''' + + +from cloudtool.utils import describe +import urllib +import urllib2 +import os +import xml.dom.minidom +import re +import base64 +import hmac +import hashlib +import httplib + +class CloudAPI: + + @describe("server", "Management Server host name or address") + @describe("apikey", "Management Server apiKey") + @describe("securitykey", "Management Server securityKey") + @describe("responseformat", "Response format: xml or json") + @describe("stripxml", "True if xml tags have to be stripped in the output, false otherwise") + def __init__(self, + server="127.0.0.1:8096", + responseformat="xml", + stripxml="true", + apiKey=None, + securityKey=None + ): + self.__dict__.update(locals()) + + def _make_request_with_keys(self,command,requests={}): + requests["command"] = command + requests["apiKey"] = self.apiKey + requests["response"] = "xml" + requests = zip(requests.keys(), requests.values()) + requests.sort(key=lambda x: str.lower(x[0])) + + requestUrl = "&".join(["=".join([request[0], urllib.quote_plus(str(request[1]))]) for request in requests]) + hashStr = "&".join(["=".join([str.lower(request[0]), urllib.quote_plus(str.lower(str(request[1])))]) for request in requests]) + + sig = urllib.quote_plus(base64.encodestring(hmac.new(self.securityKey, hashStr, hashlib.sha1).digest()).strip()) + + requestUrl += "&signature=%s"%sig + return requestUrl + + + def _make_request_with_auth(self, command, requests): + self.connection = httplib.HTTPConnection("%s"%(self.server)) + requests["command"] = command + requests["apiKey"] = self.apiKey + requests["response"] = self.responseformat + requests = zip(requests.keys(), requests.values()) + requests.sort(key=lambda x: str.lower(x[0])) + + requestUrl = "&".join(["=".join([request[0], urllib.quote(str(request[1],""))]) for request in requests]) + hashStr = "&".join(["=".join([str.lower(request[0]), urllib.quote(str.lower(str(request[1])),"")]) for request in requests]) + + sig = urllib.quote_plus(base64.encodestring(hmac.new(self.securityKey, str.lower(hashStr), hashlib.sha1).digest()).strip()) + + requestUrl += "&signature=%s"%sig + + self.connection.request("GET", "/client/api?%s"%requestUrl) + return self.connection.getresponse().read() + + 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 + "/client/api?" + + if not parameters: parameters = {} + if self.apiKey is not None and self.securityKey is not None: + return self._make_request_with_auth(command, parameters) + else: + parameters["command"] = command + parameters["response"] = self.responseformat + querystring = urllib.urlencode(parameters) + + url += querystring + + f = urllib2.urlopen(url) + data = f.read() + if self.stripxml == "true": + data=re.sub("<\?.*\?>", "\n", data); + data=re.sub("", "\n", data); + data=data.replace(">", "="); + data=data.replace("=<", "\n"); + data=data.replace("\n<", "\n"); + data=re.sub("\n.*cloud-stack-version=.*", "", data); + data=data.replace("\n\n\n", "\n"); + + 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 = getText(cmd.getElementsByTagName('description')[0].childNodes).strip() + if description: + description = '"""%s"""' % description + else: description = '' + arguments = [] + options = [] + descriptions = [] + + for param in cmd.getElementsByTagName("request")[0].getElementsByTagName("arg"): + argname = getText(param.getElementsByTagName('name')[0].childNodes).strip() + assert argname + + required = getText(param.getElementsByTagName('required')[0].childNodes).strip() + if required == 'true': required = True + elif required == 'false': required = False + else: raise AssertionError, "Not reached" + if required: arguments.append(argname) + options.append(argname) + + #import ipdb; ipdb.set_trace() + requestDescription = param.getElementsByTagName('description') + if requestDescription: + descriptionParam = getText(requestDescription[0].childNodes) + else: + descriptionParam = '' + if descriptionParam: descriptions.append( (argname,descriptionParam) ) + + funcparams = ["self"] + [ "%s=None"%o for o in options ] + funcparams = ", ".join(funcparams) + + code = """ + def %s(%s): + %s + parms = dict(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 + + diff --git a/cloud-cli/cloudtool/__init__.py b/cloud-cli/cloudtool/__init__.py new file mode 100644 index 00000000000..c4b479eb790 --- /dev/null +++ b/cloud-cli/cloudtool/__init__.py @@ -0,0 +1,71 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + + +''' +Created on Aug 2, 2010 + +''' + +import sys +import cloudapis as apis +import cloudtool.utils as utils + + +def main(argv=None): + + #import ipdb; ipdb.set_trace() + if argv == None: + argv = sys.argv + + prelim_args = [ x for x in argv[0:] if not x.startswith('-') ] + parser = utils.get_parser() + + api = __import__("cloudapis") + apis = getattr(api, "implementor") + if len(prelim_args) == 1: + commandlist = utils.get_command_list(apis) + parser.error("you need to specify a command name as the first 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])) + + argv = argv[1:] + if len(argv) == 1: + argv.append("--help") + + parser = utils.get_parser(apis.__init__,command) + 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[0]) + + # 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) diff --git a/cloud-cli/cloudtool/utils.py b/cloud-cli/cloudtool/utils.py new file mode 100644 index 00000000000..e123434614a --- /dev/null +++ b/cloud-cli/cloudtool/utils.py @@ -0,0 +1,169 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + + +''' +Created on Aug 2, 2010 + +''' + + +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 = "" + cmd_name = "" + 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") + + 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: + 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: + 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(api): + apilist = [] + for cmd_name in dir(api): + cmd = getattr(api,cmd_name) + if callable(cmd) and not cmd_name.startswith("_"): + apilist.append(cmd_name) + 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" % (cmd_name.replace('_','-')) ) + return cmds