# # rhevreg: register an image # # Copyring (C) 2011 Red Hat, Inc. # # requires: # ElementTree as built into Python 2.6.2 (xml.etree) # import sys import os import errno import subprocess # import exceptions import httplib import urlparse import base64 import string import json import uuid import time import stat # import xml.etree.ElementTree -- meh, code gets too verbose # from xml.etree import ElementTree as et -- hate renaming, it's like typedef from xml.etree import ElementTree TAG = "rhevreg" NFSUID = 36 NFSGID = 36 NULID = "00000000-0000-0000-0000-000000000000" def Usage(): print >>sys.stderr, "Usage: "+TAG+" ["+TAG+".conf]" sys.exit(1) def cfg_verify(cfg, key): if not cfg.has_key(key): print >>sys.stderr, TAG+": Error in configuration, missing key:", key sys.exit(1) def apipaths(connection, headers, path): connection.request("GET", path, '', headers) response = connection.getresponse() if response.status != 200: print >>sys.stderr, TAG+": API error: `GET "+path+"' status:", \ response.status sys.exit(2) body = response.read() # XXX can body be None? Exception? # Apparently ElementTree does not throw its own exceptions, so no try:. etroot = ElementTree.fromstring(body) if etroot == None: print >>sys.stderr, TAG+": API error: cannot parse root for `api'" sys.exit(2) if etroot.tag != 'api': print >>sys.stderr, TAG+": API error: root is not `api'" sys.exit(2) #etsumtot = etroot.find("summary/storage_domains/total") #if etsumtot == None: # print >>sys.stderr, TAG+": API error: cannot find "+\ # "summary/storage_domains/total" # sys.exit(2) #etsumact = etroot.find("summary/storage_domains/active") #if etsumact == None: # print >>sys.stderr, TAG+": API error: cannot find "+\ # "summary/storage_domains/active" # sys.exit(2) #print "Storage Domains: total", etsumtot.text, "active", etsumact.text pathsd = None for et in etroot: if et.tag != 'link': continue if et.attrib.get('rel') != "storagedomains": continue pathsd = et.attrib.get('href') pathdc = None for et in etroot: if et.tag != 'link': continue if et.attrib.get('rel') != "datacenters": continue pathdc = et.attrib.get('href') if pathsd == None: print >>sys.stderr, TAG+": API error: no path for storagedomains" sys.exit(2) if pathdc == None: print >>sys.stderr, TAG+": API error: no path for datacenters" sys.exit(2) return (pathsd, pathdc) def apistordom(cfg, connection, headers, pathsd): # XXX GET /rhevm-api/storagedomains/ crashes server, see bz#670397 # So for now we ignore the pathsd and use a query instead. #connection.request("GET", pathsd, '', headers) connection.request("GET", \ "/rhevm-api/storagedomains/?search=type%20%21%3D%20fcp", '', headers) response = connection.getresponse() if response.status != 200: print >>sys.stderr, TAG+": API error: `GET "+pathsd+"' status:", \ response.status sys.exit(2) body = response.read() etroot = ElementTree.fromstring(body) if etroot == None: print >>sys.stderr, \ TAG+": API error: cannot parse root for `storage_domains'" sys.exit(2) if etroot.tag != 'storage_domains': print >>sys.stderr, TAG+": API error: root is not `storage_domains'" sys.exit(2) # If we weren't lame, we'd return a class, not raw dictionary. # We are not json, we know the fields ahead of time. But we're lazy. # The C version returns a struct, naturally, so it's all good in the end. sd = None for et in etroot: if et.tag != 'storage_domain': continue ettype = et.find("type") if ettype == None: continue if ettype.text != "EXPORT": continue etstype = et.find("storage/type") if etstype == None: continue if etstype.text != 'NFS': continue uuidsd = et.attrib.get('id') if uuidsd == None: print >>sys.stderr, TAG+": NFS storage domain without UUID" continue etaddr = et.find("storage/address") if etaddr == None: print >>sys.stderr, TAG+": NFS storage domain without address:", \ et.find("name").text continue etpath = et.find("storage/path") if etpath == None: print >>sys.stderr, TAG+": NFS storage domain without path:", \ et.find("name").text continue # XXX canonicalize host in case of e.g. raw IPv4/IPv6 address. if etaddr.text != cfg['nfshost']: print "Host `"+etaddr.text+\ "' does not match cfg `"+cfg['nfshost']+"'" continue if etpath.text != cfg['nfspath']: print "Path `"+etaddr.text+\ "' does not match cfg `"+cfg['nfspath']+"'" continue sd = {} sd['uuid'] = uuidsd sd['address'] = etaddr.text sd['path'] = etpath.text if sd == None: print >>sys.stderr, TAG+": NFS storage domain for `"+\ cfg['nfshost']+':'+cfg['nfspath']+"' not found" sys.exit(1) return sd def apipoolid(cfg, connection, headers, pathsds, sd_uuid): connection.request("GET", pathsds, '', headers) response = connection.getresponse() if response.status != 200: print >>sys.stderr, TAG+": API error: `GET "+pathsds+"' status:", \ response.status sys.exit(2) body = response.read() etroot = ElementTree.fromstring(body) if etroot == None: print >>sys.stderr, \ TAG+": API error: cannot parse root for `storage_domains'" sys.exit(2) if etroot.tag != 'storage_domains': print >>sys.stderr, TAG+": API error: root is not `storage_domains'" sys.exit(2) for etsd in etroot: if etsd.tag != 'storage_domain': continue xid = etsd.attrib.get('id') if xid == None: print >>sys.stderr, TAG+": storage domain without UUID" continue if xid == sd_uuid: return 1 return 0 def apipool(cfg, connection, headers, pathdc, sd_uuid): connection.request("GET", pathdc, '', headers) response = connection.getresponse() if response.status != 200: print >>sys.stderr, TAG+": API error: `GET "+pathdc+"' status:", \ response.status sys.exit(2) body = response.read() etroot = ElementTree.fromstring(body) if etroot == None: print >>sys.stderr, \ TAG+": API error: cannot parse root for `data_centers'" sys.exit(2) if etroot.tag != 'data_centers': print >>sys.stderr, TAG+": API error: root is not `data_centers'" sys.exit(2) for etdc in etroot: if etdc.tag != 'data_center': continue uuiddc = etdc.attrib.get('id') if uuiddc == None: print >>sys.stderr, TAG+": data center without UUID" continue for etlink in etdc: # most likely it's not even a link, but "name" or "version" if etlink.tag != 'link': continue rel = etlink.attrib.get('rel') if rel == None: continue # there's a bunch of other links like "files", "permissions" if rel != 'storagedomains': continue href = etlink.attrib.get('href') if href == None: print >>sys.stderr, TAG+": data canter link without href" continue if apipoolid(cfg, connection, headers, href, sd_uuid): # StoragePoolId is Data Center's UUID return uuiddc print >>sys.stderr, \ TAG+": Error: pool not found for storage domain "+sd_uuid sys.exit(2) def apistart(cfg): # Note that using a proper URL parser means that we drop query and # fragment from the configured URL. Fortunately, it is never a factor # in RHEV-M API, due to its RESTful nature. scheme, host, path, u_par, u_query, u_frag = urlparse.urlparse(cfg['apiurl']) connection = httplib.HTTPConnection(host) headers = {} authraw = cfg['apiuser'] + ':' + cfg['apipass'] authval = 'Basic ' + string.strip(base64.encodestring(authraw)) headers['Authorization'] = authval; # Step 1, fetch the API root # This is largely a formality, because if we know names of resources, # why not to fetch resources directly? But the docs imply we should # jump through this hoop. pathsd, pathdc = apipaths(connection, headers, path) # We should pull the API version and check that it's more than 2.3 XXX # Step 2, connect again, get domains sd = apistordom(cfg, connection, headers, pathsd) # Step 3, find the "storage pool ID" # This is crazy to be a separate step. You'd expect the storage domain # descriptor itself contain its pool, to be saved on Step 2. But no. sd['poolid'] = apipool(cfg, connection, headers, pathdc, sd['uuid']) connection.close() return sd def spitovf(cfg, sd, img_uuid, vol_uuid, vol_size): domdir = cfg['nfsdir']+'/'+sd['uuid'] tpl_uuid = uuid.uuid4() now = time.gmtime() etroot = ElementTree.Element('ovf:Envelope') etroot.set('xmlns:ovf', "http://schemas.dmtf.org/ovf/envelope/1/") etroot.set('xmlns:rasd', "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData") etroot.set('xmlns:vssd', "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData") etroot.set('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance") etroot.set('ovf:version', "0.9") etref = ElementTree.Element('References') etfile = ElementTree.Element('File') etfile.set('ovf:href', str(img_uuid)+'/'+str(vol_uuid)) etfile.set('ovf:id', str(vol_uuid)) etfile.set('ovf:size', str(vol_size)) etfile.set('ovf:description', os.path.basename(cfg['image'])) etref.append(etfile) etroot.append(etref) etsec = ElementTree.Element('Section') etsec.set('xsi:type', "ovf:NetworkSection_Type") ete = ElementTree.Element('Info') ete.text = "List of Networks" etsec.append(ete) # dummy section, even though we have Ethernet defined below etroot.append(etsec) etsec = ElementTree.Element('Section') etsec.set('xsi:type', "ovf:DiskSection_Type") etdisk = ElementTree.Element('Disk') etdisk.set('ovf:diskId', str(vol_uuid)) vol_size_str = str((vol_size + (1024*1024*1024) - 1) / (1024*1024*1024)) etdisk.set('ovf:size', vol_size_str) etdisk.set('ovf:actual_size', vol_size_str) etdisk.set('ovf:vm_snapshot_id', NULID) etdisk.set('ovf:fileRef', str(img_uuid)+'/'+str(vol_uuid)) etdisk.set('ovf:format',\ 'http://www.vmware.com/technical-resources/interfaces/vmdk_access.html') etdisk.set('ovf:volume-format', "RAW") etdisk.set('ovf:volume-type', "Preallocated") etdisk.set('ovf:disk-interface', "VirtIO") etdisk.set('ovf:disk-type', "System") etdisk.set('ovf:boot', "true") etdisk.set('ovf:wipe-after-delete', "false") etsec.append(etdisk) etroot.append(etsec) etcon = ElementTree.Element('Content') etcon.set('xsi:type', "ovf:VirtualSystem_Type") etcon.set('ovf:id', "out") ete = ElementTree.Element('Name') ete.text = os.path.basename(cfg['image']) etcon.append(ete) ete = ElementTree.Element('TemplateId') ete.text = str(tpl_uuid) etcon.append(ete) # spec also has 'TemplateName' ete = ElementTree.Element('Description') ete.text = "Template by iwhd" etcon.append(ete) ete = ElementTree.Element('Domain') # AD domain, not in use right now # ete.text = etcon.append(ete) ete = ElementTree.Element('CreationDate') ete.text = time.strftime("%Y/%m/%d %H:%M:%S", now) etcon.append(ete) ete = ElementTree.Element('TimeZone') # ete.text = etcon.append(ete) ete = ElementTree.Element('IsAutoSuspend') ete.text = "false" etcon.append(ete) ete = ElementTree.Element('VmType') ete.text = "1" etcon.append(ete) ete = ElementTree.Element('default_display_type') # vnc = 0, gxl = 1 ete.text = "0" etcon.append(ete) ete = ElementTree.Element('default_boot_sequence') # C=0, DC=1, N=2, CDN=3, CND=4, DCN=5, DNC=6, NCD=7, # NDC=8, CD=9, D=10, CN=11, DN=12, NC=13, ND=14 # (C - HardDisk, D - CDROM, N - Network) ete.text = "1" etcon.append(ete) etsec = ElementTree.Element('Section') etsec.set('xsi:type', "ovf:OperatingSystemSection_Type") etsec.set('ovf:id', str(tpl_uuid)) etsec.set('ovf:required', "false") ete = ElementTree.Element('Info') ete.text = "Guest OS" etsec.append(ete) ete = ElementTree.Element('Description') # This is rigid, must be "Other", "OtherLinux", "RHEL6", or such. ete.text = "OtherLinux" etsec.append(ete) etcon.append(etsec) etsec = ElementTree.Element('Section') etsec.set('xsi:type', "ovf:VirtualHardwareSection_Type") ete = ElementTree.Element('Info') ete.text = "1 CPU, 512 Memory" etsec.append(ete) etsys = ElementTree.Element('System') # This is probably wrong, needs actual type. ete = ElementTree.Element('vssd:VirtualSystemType') ete.text = "RHEVM 4.6.0.163" etsys.append(ete) etsec.append(etsys) etitem = ElementTree.Element('Item') ete = ElementTree.Element('rasd:Caption') ete.text = "1 virtual CPU" etitem.append(ete) ete = ElementTree.Element('rasd:Description') ete.text = "Number of virtual CPU" etitem.append(ete) ete = ElementTree.Element('rasd:InstanceId') ete.text = "1" etitem.append(ete) ete = ElementTree.Element('rasd:ResourceType') ete.text = "3" etitem.append(ete) ete = ElementTree.Element('rasd:num_of_sockets') ete.text = "1" etitem.append(ete) ete = ElementTree.Element('rasd:cpu_per_socket') ete.text = "1" etitem.append(ete) etsec.append(etitem) etitem = ElementTree.Element('Item') ete = ElementTree.Element('rasd:Caption') ete.text = "512 MB of memory" etitem.append(ete) ete = ElementTree.Element('rasd:Description') ete.text = "Memory Size" etitem.append(ete) ete = ElementTree.Element('rasd:InstanceId') ete.text = "2" etitem.append(ete) ete = ElementTree.Element('rasd:ResourceType') ete.text = "4" etitem.append(ete) ete = ElementTree.Element('rasd:AllocationUnits') ete.text = "MegaBytes" etitem.append(ete) ete = ElementTree.Element('rasd:VirtualQuantity') ete.text = "512" etitem.append(ete) etsec.append(etitem) etitem = ElementTree.Element('Item') ete = ElementTree.Element('rasd:Caption') ete.text = "Drive 1" etitem.append(ete) ete = ElementTree.Element('rasd:InstanceId') ete.text = str(vol_uuid) etitem.append(ete) ete = ElementTree.Element('rasd:ResourceType') ete.text = "17" etitem.append(ete) ete = ElementTree.Element('rasd:HostResource') ete.text = str(img_uuid)+'/'+str(vol_uuid) etitem.append(ete) ete = ElementTree.Element('rasd:Parent') ete.text = NULID etitem.append(ete) ete = ElementTree.Element('rasd:Template') ete.text = NULID etitem.append(ete) ete = ElementTree.Element('rasd:ApplicationList') # List of installed applications, separated by comma etitem.append(ete) # This corresponds to ID of volgroup in host where snapshot was taken. # Obviously we have nothing like it. ete = ElementTree.Element('rasd:StorageId') # "Storage Domain Id" ete.text = NULID etitem.append(ete) ete = ElementTree.Element('rasd:StoragePoolId') ete.text = sd['poolid'] etitem.append(ete) ete = ElementTree.Element('rasd:CreationDate') ete.text = time.strftime("%Y/%m/%d %H:%M:%S", now) etitem.append(ete) ete = ElementTree.Element('rasd:LastModified') ete.text = time.strftime("%Y/%m/%d %H:%M:%S", now) etitem.append(ete) etsec.append(etitem) etitem = ElementTree.Element('Item') ete = ElementTree.Element('rasd:Caption') ete.text = "Ethernet 0 rhevm" etitem.append(ete) ete = ElementTree.Element('rasd:InstanceId') ete.text = "3" etitem.append(ete) ete = ElementTree.Element('rasd:ResourceType') ete.text = "10" etitem.append(ete) ete = ElementTree.Element('rasd:ResourceSubType') # e1000 = 2, pv = 3 ete.text = "3" etitem.append(ete) ete = ElementTree.Element('rasd:Connection') ete.text = "rhevm" etitem.append(ete) ete = ElementTree.Element('rasd:Name') ete.text = "eth0" etitem.append(ete) # also allowed is "MACAddress" ete = ElementTree.Element('rasd:speed') ete.text = "1000" etitem.append(ete) etsec.append(etitem) etitem = ElementTree.Element('Item') ete = ElementTree.Element('rasd:Caption') ete.text = "Graphics" etitem.append(ete) ete = ElementTree.Element('rasd:InstanceId') # doc says "6", reality is "5" ete.text = "5" etitem.append(ete) ete = ElementTree.Element('rasd:ResourceType') ete.text = "20" etitem.append(ete) ete = ElementTree.Element('rasd:VirtualQuantity') ete.text = "1" etitem.append(ete) etsec.append(etitem) etcon.append(etsec) etroot.append(etcon) tmpovfdir = domdir+"/iwhd."+str(tpl_uuid) try: os.mkdir(tmpovfdir) except OSError, e: if e.errno != errno.EEXIST: print >>sys.stderr, TAG+": Cannot make directory :", e sys.exit(1) et = ElementTree.ElementTree(etroot) tmpovf = tmpovfdir+"/"+str(tpl_uuid)+".ovf" et.write(tmpovf, encoding="UTF-8") tmpimgdir = domdir+"/iwhd."+str(img_uuid) imgdir = domdir+"/images/"+str(img_uuid) os.rename(tmpimgdir, imgdir) ovfdir = domdir+"/master/vms/"+str(tpl_uuid) os.rename(tmpovfdir, ovfdir) return tpl_uuid def copyimage(cfg, sd): ## We do not want to run as root, and we do not want to garbage-collect ## old mounts, so we expect the user to mount everything ahead of time. ## #if os.geteuid() != 0: # print >>sys.stderr, TAG+": Have to run as root" # sys.exit(1) # #nfsdir = cfg['nfsdir'] #try: # os.mkdir(nfsdir) #except OSError, e: # if e.errno != errno.EEXIST: # print >>sys.stderr, TAG+": Cannot make directory :", e # sys.exit(1) # #export = cfg['nfshost']+":"+cfg['nfspath'] #rc = subprocess.call(["/bin/mount", "-t", "nfs", "-o", "nolock", export, nfsdir]) #if rc != 0: # print >>sys.stderr, TAG+": cannot mount "+export+" on "+nfsdir+" :", rc # sys.exit(1) if os.geteuid() == 0: #print "We're root, changing user and group to 36" # N.B. -1 is not a valid input in Python. os.setregid(NFSGID, NFSGID); os.setreuid(NFSUID, NFSUID); else: if os.geteuid() != NFSUID or os.getegid() != NFSGID: print >>sys.stderr, TAG+": Have to run with user and group 36" sys.exit(1) domdir = cfg['nfsdir']+'/'+sd['uuid'] if not os.path.isdir(domdir): print >>sys.stderr, TAG+": path `"+domdir+"' is not a directory" sys.exit(1) # Note that uuid1 called twice quickly returns the same UUID, so uuid4. # XXX Check what the first uuid call does (does it run anything?) vol_uuid = uuid.uuid4() img_uuid = uuid.uuid4() # copy stuff # XXX Do something about garbage-collecting iwhd.* directories tmpimgdir = domdir+"/iwhd."+str(img_uuid) try: os.mkdir(tmpimgdir) except OSError, e: # errno.EEXIST is an error here too (loss of uniqueness) print >>sys.stderr, TAG+": Cannot make directory :", e sys.exit(1) now = int(time.time()) imgsrc = cfg['image'] imgdst = tmpimgdir+'/'+str(vol_uuid) rc = subprocess.call(["/bin/cp", imgsrc, imgdst]) if rc != 0: print >>sys.stderr, TAG+": cannot copy "+imgsrc+" to "+imgdst+" :", rc sys.exit(1) statb = os.stat(imgdst) vol_size = statb[stat.ST_SIZE] imgmeta = imgdst+".meta" try: fp = open(imgmeta, "w") except IOError, e: print >>sys.stderr, TAG+": Error opening OVF:", e sys.exit(1) print >>fp, "DOMAIN="+sd['uuid'] # saved template has VOLTYPE=SHARED print >>fp, "VOLTYPE=LEAF" print >>fp, "CTIME="+str(now) # saved template has FORMAT=COW print >>fp, "FORMAT=RAW" print >>fp, "IMAGE="+str(img_uuid) print >>fp, "DISKTYPE=1" print >>fp, "PUUID="+NULID print >>fp, "LEGALITY=LEGAL" print >>fp, "MTIME="+str(now) print >>fp, "POOL_UUID="+sd['poolid'] # assuming 1KB alignment, so 512 is no problem print >>fp, "SIZE="+str(vol_size/512) print >>fp, "TYPE=SPARSE" print >>fp, "DESCRIPTION=Uploaded by iwhd+rhevreg" print >>fp, "EOF" fp.close() return spitovf(cfg, sd, img_uuid, vol_uuid, vol_size) def main(): argc = len(sys.argv) if argc == 1: cfgname = TAG+".conf" elif argc == 2: cfgname = sys.argv[1] if len(cfgname) == 0 or cfgname[0] == '-': Usage() else: Usage() try: fp = open(cfgname, 'r') except IOError, e: # printing e outpus the filename too, so no explicit cfgname in printout print >>sys.stderr, TAG+": Error opening configuration:", e sys.exit(1) try: cfg = json.load(fp) except ValueError, e: print >>sys.stderr, TAG+": Error parsing configuration:", e sys.exit(1) fp.close() # Verify the class members here, so that we do not traceback unexpectedly. # # image: local filename with disk image cfg_verify(cfg, 'image') if not os.path.exists(cfg['image']): print >>sys.stderr, TAG+": Image `"+cfg['image']+"' does not exist" sys.exit(1) # apiurl: the so-called "base", usually "rhev-api" cfg_verify(cfg, 'apiurl') # apiuser: username@AD.domain # We do not enforce with '@' syntax in case someone comes up with RHEV-M # that takes local authentication of some other trick. cfg_verify(cfg, 'apiuser') # apipass: password for apiuser cfg_verify(cfg, 'apipass') # nfshost: NFS server name # nfspath: export path # All RHEV-M servers have several storage domains, so we use this to # select one. Also, we verify if 'nfsdir' points where it should. cfg_verify(cfg, 'nfshost') cfg_verify(cfg, 'nfspath') # nfsdir: A directory where sysadmin or boot scripts or autofs mounted # the same area that RHEV-M considers an export domain (/mnt/vdsm/rhevm23). # N.B. If iwhd is running on the NFS server itself, nfsdir can be the # exported directory itself (like /home/vdsm/rhevm23). We verify that # the directory contains an expected structure, so attach it to RHEV-M # before trying to register any images. cfg_verify(cfg, 'nfsdir') d = cfg['nfsdir'] if len(d) == 0: print >>sys.stderr, TAG+": Error in configuration, `nfsdir' is empty" sys.exit(1) if not os.path.isabs(d): print >>sys.stderr, \ TAG+": Error in configuration, `nfsdir' is relative:", d sys.exit(1) sd = apistart(cfg) tpl_uuid = copyimage(cfg, sd) print "IMAGE", str(tpl_uuid) # http://utcc.utoronto.ca/~cks/space/blog/python/ImportableMain # TODO: replace sys.exit with exceptions (check hailcampack.py ConfigError) if __name__ == "__main__": main()