initial checkin of qa api
authorTony Mack <tmack@cs.princeton.edu>
Mon, 7 Jan 2008 15:24:25 +0000 (15:24 +0000)
committerTony Mack <tmack@cs.princeton.edu>
Mon, 7 Jan 2008 15:24:25 +0000 (15:24 +0000)
21 files changed:
qaapi/System.py [new file with mode: 0644]
qaapi/TestAPI.py [new file with mode: 0755]
qaapi/qa/Config.py [new file with mode: 0644]
qaapi/qa/Makefile [new file with mode: 0644]
qaapi/qa/QAAPI.py [new file with mode: 0644]
qaapi/qa/Test.py [new file with mode: 0644]
qaapi/qa/__init__.py [new file with mode: 0644]
qaapi/qa/logger.py [new file with mode: 0644]
qaapi/qa/modules/__init__.py [new file with mode: 0644]
qaapi/qa/modules/api/__init__.py [new file with mode: 0644]
qaapi/qa/modules/plc/__init__.py [new file with mode: 0644]
qaapi/qa/modules/plc/configure.py [new file with mode: 0644]
qaapi/qa/modules/plc/install.py [new file with mode: 0644]
qaapi/qa/modules/plc/start.py [new file with mode: 0644]
qaapi/qa/modules/plc/stop.py [new file with mode: 0644]
qaapi/qa/modules/plc/uninstall.py [new file with mode: 0644]
qaapi/qa/qa_config [new file with mode: 0644]
qaapi/qa/tests/__init__.py [new file with mode: 0644]
qaapi/qa/utils.py [new file with mode: 0644]
qaapi/qash [new file with mode: 0755]
qaapi/setup.py [new file with mode: 0755]

diff --git a/qaapi/System.py b/qaapi/System.py
new file mode 100644 (file)
index 0000000..400a7b9
--- /dev/null
@@ -0,0 +1,147 @@
+from optparse import OptionParser
+from logger import log 
+import utils
+import os, sys
+import traceback
+from Config import Config
+
+
+class System:
+    """
+    Installs a myplc and starts the service. This is required
+    before any other tests can take place.
+    """        
+
+    def __init__(self):
+       self.path=os.path.dirname(sys.argv[0])
+       self.config = Config()
+       self.errors = []
+
+    def check(method, fatal = True):
+       """
+       Check if there were errors
+       """
+       def wrapper(*args, **kwds):
+           result = method(*args, **kwds)
+           if self.errors:
+               print "\n".join(self.errors)
+               if fatal: sys.exit(1)
+           return result
+
+    @check     
+    @log
+    def get_url(self):
+       """
+       Determine current url and save it
+       """
+       try:
+           if len(self.args) == 1: self.url = self.args[0]
+           else:
+               url_file=open("%s/URL"%self.path)
+                self.url=url_file.read().strip()
+                url_file.close()
+               
+           if self.options.verbose:
+               utils.header('Saving current myplc url into %s/URL' % self.path)
+           fsave=open('%s/URL'%self.path,"w")
+            fsave.write(url+'\n')
+           fsave.close()       
+       except:
+           self.errors = ["Cannot determine myplc url"]
+            self.parser.print_help()
+           raise
+
+    @check
+    @log
+    def install_plc(self):
+       """
+       Install the myplc
+       """
+       if self.options.verbose:
+           utils.header("Installing myplc from url %s" % self.url)
+       (stdin, stdout, stderr) = os.popen3('set -x; rpm -Uvh ' + self.url)
+       self.errors = stderr.readlines()
+       if self.errors: raise "\n".join(self.errors)
+       
+
+    @check
+    @log       
+    def config_plc(self):
+       """ 
+       Configure the plc
+       """
+       tmpname='/tmp/plc-config-tty-%d'%os.getpid()
+        fileconf=open(tmpname,'w')
+        for var in [ 'PLC_NAME',
+                     'PLC_ROOT_PASSWORD',
+                     'PLC_ROOT_USER',
+                     'PLC_MAIL_ENABLED',
+                     'PLC_MAIL_SUPPORT_ADDRESS',
+                     'PLC_DB_HOST',
+                     'PLC_API_HOST',
+                     'PLC_WWW_HOST',
+                     'PLC_BOOT_HOST',
+                     'PLC_NET_DNS1',
+                     'PLC_NET_DNS2']:
+            fileconf.write ('e %s\n%s\n'%(var,config[var]))
+        fileconf.write('w\nq\n')
+        fileconf.close()
+
+        if self.options.verbose: os.system('set -x ; cat %s'%tmpname)
+        (stdin, stdout, stderr) = \
+            os.popen3('set -x ; chroot /plc/root  plc-config-tty < %s'%tmpname)
+       self.errors = stderr.readlines()
+       if self.errors: raise "\n".join(self.errors)
+       os.system('set -x; rm %s'%tmpname)
+    @check 
+    @log 
+    def start_plc(self):
+       """ 
+       Start the plc service
+        """
+       (stdin, stdout, stderr) = os.popen3('set -x ; service plc start')
+       self.errors = stderr.readlines()
+       if self.errors: raise "\n".join(self.errors)
+    @check 
+    @log  
+    def remove_plc(self):
+       """
+       Completely remove all traces of myplc install
+       """
+       if self.options.verbose:
+            utils.header("Removing myplc")
+        (stdin, stdout, stderr) = os.popen3('set -x; service plc safestop')
+       self.errors = stderr.readlines()
+        (stdin, stdout, stderr) = os.popen3('set -x; rpm -e myplc')
+       self.errors.extend(stderr.readlines())
+        if self.errors: raise "\n".join(self.errors)
+       (stdin, stdout, stderr) = os.popen3('set -x; rm -rf  /plc/data')
+       self.errors.extend(stderr.readlines())
+       if self.errors: raise "\n".join(self.errors)
+
+    def run(self):
+       try:
+            usage = """usage: %prog [options] [myplc-url]
+           myplc-url defaults to the last value used, 
+           as stored in URL"""
+            self.parser=OptionParser(usage=usage)
+
+            #parser.add_option("-d","--display", action="store", dest="Xdisplay", default='bellami:0.0',
+            #                  help="sets DISPLAY for vmplayer")
+            self.parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
+                              help="Run in verbose mode")
+            (self.options, self.args) = self.parser.parse_args()
+           
+           self.get_url()
+           self.install_plc()
+
+       except:
+           pass        
+
+
+
+if __name__ == 'main':
+    System().run()     
diff --git a/qaapi/TestAPI.py b/qaapi/TestAPI.py
new file mode 100755 (executable)
index 0000000..bf1b80e
--- /dev/null
@@ -0,0 +1,678 @@
+#!/usr/bin/python
+#
+# Test script example
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Test.py,v 1.18 2007/01/09 16:22:49 mlhuang Exp $
+#
+
+from pprint import pprint
+from string import letters, digits, punctuation
+from traceback import print_exc
+from optparse import OptionParser
+import base64
+import os
+import socket
+import xmlrpclib
+
+from Config import Config
+from logger import log
+from random import Random
+random = Random()
+
+config = Config()
+api = config.api
+auth = api.auth
+
+boot_states = api.GetBootStates(auth)
+roles = [role['name'] for role in api.GetRoles(auth)]
+methods = api.GetNetworkMethods(auth)
+types = api.GetNetworkTypes(auth)
+
+#ifrom PLC.Shell import Shell
+#shell = Shell(globals())
+
+def randfloat(min = 0.0, max = 1.0):
+    return float(min) + (random.random() * (float(max) - float(min)))
+
+def randint(min = 0, max = 1):
+    return int(randfloat(min, max + 1))
+
+# See "2.2 Characters" in the XML specification:
+#
+# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
+# avoiding
+# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
+
+ascii_xml_chars = map(unichr, [0x9, 0xA, 0xD])
+ascii_xml_chars += map(unichr, xrange(0x20, 0x7F - 1))
+low_xml_chars = list(ascii_xml_chars)
+low_xml_chars += map(unichr, xrange(0x84 + 1, 0x86 - 1))
+low_xml_chars += map(unichr, xrange(0x9F + 1, 0xFF))
+valid_xml_chars = list(low_xml_chars)
+valid_xml_chars += map(unichr, xrange(0xFF + 1, 0xD7FF))
+valid_xml_chars += map(unichr, xrange(0xE000, 0xFDD0 - 1))
+valid_xml_chars += map(unichr, xrange(0xFDDF + 1, 0xFFFD))
+
+
+def randstr(length, pool = valid_xml_chars, encoding = "utf-8"):
+    sample = random.sample(pool, min(length, len(pool)))
+    while True:
+        s = u''.join(sample)
+        bytes = len(s.encode(encoding))
+        if bytes > length:
+            sample.pop()
+        elif bytes < length:
+            sample += random.sample(pool, min(length - bytes, len(pool)))
+            random.shuffle(sample)
+        else:
+            break
+    return s
+
+def randhostname():
+    # 1. Each part begins and ends with a letter or number.
+    # 2. Each part except the last can contain letters, numbers, or hyphens.
+    # 3. Each part is between 1 and 64 characters, including the trailing dot.
+    # 4. At least two parts.
+    # 5. Last part can only contain between 2 and 6 letters.
+    hostname = 'a' + randstr(61, letters + digits + '-') + '1.' + \
+               'b' + randstr(61, letters + digits + '-') + '2.' + \
+               'c' + randstr(5, letters)
+    return hostname
+
+def randpath(length):
+    parts = []
+    for i in range(randint(1, 10)):
+        parts.append(randstr(randint(1, 30), ascii_xml_chars))
+    return os.sep.join(parts)[0:length]
+
+def randemail():
+    return (randstr(100, letters + digits) + "@" + randhostname()).lower()
+
+def randkey(bits = 2048):
+    key_types = ["ssh-dss", "ssh-rsa"]
+    key_type = random.sample(key_types, 1)[0]
+    return ' '.join([key_type,
+                     base64.b64encode(''.join(randstr(bits / 8).encode("utf-8"))),
+                     randemail()])
+
+def random_site():
+    return {
+        'name': randstr(254),
+        'abbreviated_name': randstr(50),
+        'login_base': randstr(20, letters).lower(),
+        'latitude': int(randfloat(-90.0, 90.0) * 1000) / 1000.0,
+        'longitude': int(randfloat(-180.0, 180.0) * 1000) / 1000.0,
+        }
+            
+def random_address_type():
+    return {
+        'name': randstr(20),
+        'description': randstr(254),
+        }
+
+def random_address():
+    return {
+        'line1': randstr(254),
+        'line2': randstr(254),
+        'line3': randstr(254),
+        'city': randstr(254),
+        'state': randstr(254),
+        'postalcode': randstr(64),
+        'country': randstr(128),
+        }
+
+def random_person():
+    return {
+        'first_name': randstr(128),
+        'last_name': randstr(128),
+        'email': randemail(),
+        'bio': randstr(254),
+        # Accounts are disabled by default
+        'enabled': False,
+        'password': randstr(254),
+        }
+
+def random_key():
+    return {
+        'key_type': random.sample(key_types, 1)[0],
+        'key': randkey()
+        }
+
+def random_slice():
+    return {
+        'name': site['login_base'] + "_" + randstr(11, letters).lower(),
+        'url': "http://" + randhostname() + "/",
+        'description': randstr(2048),
+        }
+
+def random_nodegroup():
+    return {
+        'name': randstr(50),
+        'description': randstr(200),
+        }
+
+def random_node():
+   return {
+       'hostname': randhostname(),
+       'boot_state': random.sample(boot_states, 1)[0],
+       'model': randstr(255),
+       'version': randstr(64),
+       }
+
+def random_nodenetwork():
+    nodenetwork_fields = {
+        'method': random.sample(methods, 1)[0],
+        'type': random.sample(types, 1)[0],
+        'bwlimit': randint(500000, 10000000),
+        }
+
+    if method != 'dhcp':
+        ip = randint(0, 0xffffffff)
+        netmask = (0xffffffff << randint(2, 31)) & 0xffffffff
+        network = ip & netmask
+        broadcast = ((ip & netmask) | ~netmask) & 0xffffffff
+        gateway = randint(network + 1, broadcast - 1)
+        dns1 = randint(0, 0xffffffff)
+
+        for field in 'ip', 'netmask', 'network', 'broadcast', 'gateway', 'dns1':
+            nodenetwork_fields[field] = socket.inet_ntoa(struct.pack('>L', locals()[field]))
+
+    return nodenetwork_fields
+
+def random_pcu():
+    return {
+        'hostname': randhostname(),
+        'ip': socket.inet_ntoa(struct.pack('>L', randint(0, 0xffffffff))),
+        'protocol': randstr(16),
+        'username': randstr(254),
+        'password': randstr(254),
+        'notes': randstr(254),
+        'model': randstr(32),
+        }
+
+def random_conf_file():
+    return {
+        'enabled': bool(randint()),
+        'source': randpath(255),
+        'dest': randpath(255),
+        'file_permissions': "%#o" % randint(0, 512),
+        'file_owner': randstr(32, letters + '_' + digits),
+        'file_group': randstr(32, letters + '_' + digits),
+        'preinstall_cmd': randpath(100),
+        'postinstall_cmd': randpath(100),
+        'error_cmd': randpath(100),
+        'ignore_cmd_errors': bool(randint()),
+        'always_update': bool(randint()),
+        }
+
+def random_attribute_type():
+    return {
+        'name': randstr(100),
+        'description': randstr(254),
+        'min_role_id': random.sample(roles.values(), 1)[0],
+        }
+
+def isequal(object_fields, expected_fields):
+    for field in expected_fields:
+       assert field in object_fields
+       assert object_fields[field] == expected_fields[field]
+
+def islistequal(list1, list2):
+    assert set(list1) == set(list2) 
+
+def isunique(id, id_list):
+    assert id not in id_list
+
+def get_methods(self, prefix):
+    method_list = filter(lambda name: name.startswith(prefix), self.methods)
+
+
+
+tests = [
+    # test_type: (Method, arguments, method_result_holder, number_to_add)      
+    {'add': (api.AddSite, random_site(), site_ids, 3),
+     'add_check': (isunique,    
+
+    ]
+
+class Entity:
+    """
+    Template class for testing Add methods
+    """        
+    def __init__(self, auth, add_method, update_method, get_method, delete_method, primary_key):    
+       
+       self.methods_tested = []
+       for method in [add_method, update_method, get_method, delete_method]:
+           self.methods_tested.append(method._Method_name)
+       
+       self.auth = auth
+       self.Add = log(add_method)
+       self.Update = log(update_method)
+       self.Get = log(get_method)
+       self.Delete = log(delete_method)
+       self.primary_key = primary_key
+       self.object_ids = []
+        
+    def test(self, args_method, num):
+       
+       self.object_ids = []
+       
+       for i in range(num):  
+           # Add object
+           object_fields = args_method()
+           object_id = self.Add(self.auth, object_fields)
+           
+           # Should return a unique id
+           AddCheck = log(isunique, 'Unique Check')
+           AddCheck(object_id, object_ids)
+           self.object_ids.extend([object_id])
+
+           # Check object
+           object = slef.Get(self.auth, [object_id])[0]
+           CheckObject = log(isequal, 'Add Check')
+           CheckObject(object, object_fields)
+           
+           # Update object
+           object_fields = args_method()
+           self.Update(self.auth, object_id, object_fields)
+
+           # Check again
+           object = self.Get(self.auth, [object_id])[0]
+           CheckObject = log(isqeual, 'Update Check')          
+           CheckSite(object, object_fields)
+
+       # Check Get all sites
+       objects = self.Get(self.auth, object_ids)
+       CheckObjects = log(islistqual, 'Get Check')
+       CheckObjects(object_ids, [object[self.primary_key] for object in objects])          
+           
+       return self.object_ids
+
+    def cleanup(self):
+       # Delete objects
+       for object_id in sefl.object_ids:
+           self.Delete(self.auth, object_id)
+           
+       # Check if objects are deleted
+       CheckObjects = log(islistequal, 'Delete Check') 
+       ChecObjects(api.Get(auth, self.object_ids), [])         
+       
+
+       
+class Test:
+    def __init__(self, config, verbose = True):
+       self.check = True
+        self.verbose = verbose
+       self.methods = set(api.system.listMethods())
+       self.methods_tested = set()
+       
+       self.site_ids = []
+        self.address_type_ids = []
+        self.address_ids = []
+        self.person_ids = []
+
+
+    def run(self,
+            sites = 1,
+            address_types = 3,
+            addresses = 2,
+            persons = 1000,
+            keys = 3):
+        try:
+            try:
+                site_test = Entity(auth, api.AddSite, api.UpdateSite, api.GetSite, api.DeleteSite, 'site_id')               site_test.test(random_site, sites)
+
+                self.AddressTypes(address_types)
+                self.AddAddresses(addresses)
+                self.AddPersons(persons)
+            except:
+                print_exc()
+        finally:
+           for method in set(self.methods).difference(self.methoods_tested):
+               print >> test_log, "%(method)s [Not Tested]" % locals() 
+
+    def cleanup(self):
+        self.DeletePersons()
+        self.DeleteAddresses()
+        self.DeleteAddressTypes()
+        self.DeleteSites()
+
+       
+
+    def Sites(self, n = 1):
+        """
+        Add and Modify a random site.
+        """
+
+       for i in range(n):
+            # Add site
+            site_fields = random_site()
+           AddSite = log(api.AddSite)
+            site_id = AddSite(auth, site_fields)
+           self.methods_tested.update(['AddSite'])             
+            
+           # Should return a unique site_id
+            CheckSite = log(isunique, 'Unique Check')
+           CheckSite(site_id, self.site_ids)
+            self.site_ids.append(site_id)
+
+            # Check site
+           GetSites = log(api.GetSites)
+            site = GetSites(auth, [site_id])[0]
+           CheckSite = log(isequal, 'AddSite Check')
+           CheckSite(site, site_fields)
+           self.methods_tested.update(['GetSites'])    
+
+            # Update site
+            site_fields = random_site()
+            # XXX Currently cannot change login_base
+            del site_fields['login_base']
+            site_fields['max_slices'] = randint(1, 10)
+            UpdateSite = log(api.UpdateSite)
+           UpdateSite(auth, site_id, site_fields)
+           self.methods_tested.update(['UpdateSite'])
+
+            # Check site again
+            site = GetSites(auth, [site_id])[0]
+           CheckSite = log(isequal, 'UpdateSite Check')
+           CheckSite(site, site_fields)
+
+         
+       # Check Get all sites   
+        sites = GetSites(auth, self.site_ids)
+        CheckSite = log(islistequal, 'GetSites Check')
+       CheckSite(self.site_ids, [site['site_id'] for site in sites])
+
+        if self.verbose:
+            print "Added sites", self.site_ids
+
+    def DeleteSites(self):
+        """
+        Delete any random sites we may have added.
+        """
+       # Delete all sites
+       DeleteSite = log(api.DeleteSite)
+        for site_id in self.site_ids:
+            DeleteSite(auth, site_id)
+       self.methods_tested.update(['DeleteSite'])
+
+       # Check if sites are deleted
+       CheckSite = log(islistequal, 'DeleteSite Check')
+       CheckSite(api.GetSites(auth, self.site_ids), [])
+
+        if self.verbose:
+            print "Deleted sites", self.site_ids
+
+        self.site_ids = []
+
+    def AddAddressTypes(self, n = 3):
+        """
+        Add a number of random address types.
+        """
+        
+        for i in range(n):
+            address_type_fields = random_address_type()
+           AddAddressType = log(api.AddAddressType)
+            address_type_id = AddAddressType(auth, address_type_fields)
+           self.methods_tested.update(['AddAddressType'])
+
+            # Should return a unique address_type_id
+            CheckAddressType = log(isunique, 'Unique Check')
+           CheckAddressType(address_type_id, self.address_type_ids)
+           self.address_type_ids.append(address_type_id)
+
+            # Check address type
+           GetAddressTypes = log(api.GetAddressTypes)
+            address_type = GetAddressTypes(auth, [address_type_id])[0]
+           CheckAddressType = log(isequal, 'AddAddressType Check')
+           CheckAddressType(address_type, address_type_fields)
+
+            # Update address type
+            address_type_fields = random_address_type()
+           UpdateAddressType = log(api.UpdateAddressType)
+            UpdateAddressType(auth, address_type_id, address_type_fields)
+            
+            # Check address type again
+            address_type = GetAddressTypes([address_type_id])[0]
+           CheckAddressType = log(isequal, 'UpdateAddressType Check')
+           CheckAddressType(address_type, address_type_fields)
+           self.methods_tested.update(['UpdateAddressType'])
+
+       # Check get all address types
+        address_types = GetAddressTypes(auth, self.address_type_ids)
+       CheckAddressType = log(islistequal, 'GetAddressType Check')
+       CheckAddressType(self.address_type_ids, 
+                        [address_type['address_type_id' for address_type in address_types])
+       self.methods_tested.update(['GetAddressTypes'])
+
+        if self.verbose:
+            print "Added address types", self.address_type_ids
+
+    def DeleteAddressTypes(self):
+        """
+        Delete any random address types we may have added.
+        """
+
+       DeleteAddressType = log(api.DeleteAddressType)
+        for address_type_id in self.address_type_ids:
+            DeleteAddressType(auth, address_type_id)
+       self.methods_tested.update(['DeleteAddressType'])
+
+       CheckAddressType = log(islistequal, 'DeleteAddressType Check')
+       CheckAddressType(api.GetAddressTypes(auth, self.address_type_ids), [])
+
+        if self.verbose:
+            print "Deleted address types", self.address_type_ids
+
+        self.address_type_ids = []
+
+    def AddAddresses(self, n = 3):
+        """
+        Add a number of random addresses to each site.
+        """
+
+        for site_id in self.site_ids:
+            for i in range(n):
+                address_fields = random_address()
+                address_id = AddSiteAddress(site_id, address_fields)
+
+                # Should return a unique address_id
+                assert address_id not in self.address_ids
+                self.address_ids.append(address_id)
+
+                if self.check:
+                    # Check address
+                    address = GetAddresses([address_id])[0]
+                    for field in address_fields:
+                        assert address[field] == address_fields[field]
+
+                    # Update address
+                    address_fields = random_address()
+                    UpdateAddress(address_id, address_fields)
+
+                    # Check address again
+                    address = GetAddresses([address_id])[0]
+                    for field in address_fields:
+                        assert address[field] == address_fields[field]
+
+                # Add address types
+                for address_type_id in self.address_type_ids:
+                    AddAddressTypeToAddress(address_type_id, address_id)
+
+        if self.check:
+            addresses = GetAddresses(self.address_ids)
+            assert set(self.address_ids) == set([address['address_id'] for address in addresses])
+            for address in addresses:
+                assert set(self.address_type_ids) == set(address['address_type_ids'])
+
+        if self.verbose:
+            print "Added addresses", self.address_ids
+
+    def DeleteAddresses(self):
+        """
+        Delete any random addresses we may have added.
+        """
+
+        # Delete site addresses
+        for address_id in self.address_ids:
+            # Remove address types
+            for address_type_id in self.address_type_ids:
+                DeleteAddressTypeFromAddress(address_type_id, address_id)
+
+            if self.check:
+                address = GetAddresses([address_id])[0]
+                assert not address['address_type_ids']
+
+            DeleteAddress(address_id)
+            if self.check:
+                assert not GetAddresses([address_id])
+
+        if self.check:
+            assert not GetAddresses(self.address_ids)
+
+        if self.verbose:
+            print "Deleted addresses", self.address_ids
+
+        self.address_ids = []
+
+    def AddPersons(self, n = 3):
+        """
+        Add a number of random users to each site.
+        """
+
+        roles = GetRoles()
+        role_ids = [role['role_id'] for role in roles]
+        roles = [role['name'] for role in roles]
+        roles = dict(zip(roles, role_ids))
+
+        for i in range(n):
+
+            # Add account
+            person_fields = random_person()
+            person_id = AddPerson(person_fields)
+
+            # Should return a unique person_id
+            assert person_id not in self.person_ids
+            self.person_ids.append(person_id)
+
+            if self.check:
+                # Check account
+                person = GetPersons([person_id])[0]
+                for field in person_fields:
+                    if field != 'password':
+                        assert person[field] == person_fields[field]
+
+                # Update account
+                person_fields = random_person()
+                UpdatePerson(person_id, person_fields)
+
+                # Check account again
+                person = GetPersons([person_id])[0]
+                for field in person_fields:
+                    if field != 'password':
+                        assert person[field] == person_fields[field]
+
+            auth = {'AuthMethod': "password",
+                    'Username': person_fields['email'],
+                    'AuthString': person_fields['password']}
+
+            if self.check:
+                # Check that account is disabled
+                try:
+                    assert not AuthCheck(auth)
+                except:
+                    pass
+
+            # Add random set of roles
+            person_roles = random.sample(['user', 'pi', 'tech'], randint(1, 3))
+            for person_role in person_roles:
+                role_id = roles[person_role]
+                AddRoleToPerson(role_id, person_id)
+
+            if self.check:
+                person = GetPersons([person_id])[0]
+                assert set(person_roles) == set(person['roles'])
+
+            # Enable account
+            UpdatePerson(person_id, {'enabled': True})
+
+            if self.check:
+                # Check that account is enabled
+                assert AuthCheck(auth)
+
+            # Associate account with random set of sites
+            person_site_ids = []
+            for site_id in random.sample(self.site_ids, randint(1, len(self.site_ids))):
+                AddPersonToSite(person_id, site_id)
+                person_site_ids.append(site_id)
+
+            if self.check:
+                # Make sure it really did it
+                person = GetPersons([person_id])[0]
+                assert set(person_site_ids) == set(person['site_ids'])
+
+            # Set a primary site
+            primary_site_id = random.sample(person_site_ids, randint(1, len(person_site_ids)))[0]
+            SetPersonPrimarySite(person_id, primary_site_id)
+
+            if self.check:
+                person = GetPersons([person_id])[0]
+                assert person['site_ids'][0] == primary_site_id
+
+        if self.verbose:
+            print "Added users", self.person_ids
+
+    def DeletePersons(self):
+        # Delete users
+        for person_id in self.person_ids:
+            # Remove from each site
+            for site_id in self.site_ids:
+                DeletePersonFromSite(person_id, site_id)
+
+            if self.check:
+                person = GetPersons([person_id])[0]
+                assert not person['site_ids']
+
+            # Revoke roles
+            person = GetPersons([person_id])[0]
+            for role_id in person['role_ids']:
+                DeleteRoleFromPerson(role_id, person_id)
+
+            if self.check:
+                person = GetPersons([person_id])[0]
+                assert not person['role_ids']
+
+            # Disable account
+            UpdatePerson(person_id, {'enabled': False})
+
+            if self.check:
+                person = GetPersons([person_id])[0]
+                assert not person['enabled']
+
+            # Delete account
+            DeletePerson(person_id)
+
+            if self.check:
+                assert not GetPersons([person_id])                         
+
+        if self.check:
+            assert not GetPersons(self.person_ids)
+
+        if self.verbose:
+            print "Deleted users", self.person_ids
+
+        self.person_ids = []
+
+if __name__ == "__main__":
+    parser = OptionParser()
+    parser.add_option("-c", "--check", action = "store_true", default = False, help = "Verify actions (default: %default)")
+    parser.add_option("-q", "--quiet", action = "store_true", default = False, help = "Be quiet (default: %default)")
+    parser.add_option("-p", "--populate", action = "store_true", default = False, help = "Do not cleanup (default: %default)")
+    (options, args) = parser.parse_args()
+    test = Test(check = options.check, verbose = not options.quiet)
+    test.run()
+    if not options.populate:
+        test.cleanup()
diff --git a/qaapi/qa/Config.py b/qaapi/qa/Config.py
new file mode 100644 (file)
index 0000000..d230d51
--- /dev/null
@@ -0,0 +1,20 @@
+import xmlrpclib
+import os
+
+class Config:
+
+    path = os.path.dirname(os.path.abspath(__file__))  
+    def __init__(self, config_file = path+os.sep+'qa_config'):
+       try:
+            execfile(config_file, self.__dict__)
+        except:
+           print __file__
+            raise "Could not find system config in %s" % config_file
+
+       self.auth = {}
+       self.auth['Username'] = self.PLC_ROOT_USER
+       self.auth['AuthString'] = self.PLC_ROOT_PASSWORD
+       self.auth['AuthMethod'] = 'password'
+       self.api = xmlrpclib.Server('https://%s/PLCAPI/' % self.PLC_API_HOST)
+       self.verbose = True     
+
diff --git a/qaapi/qa/Makefile b/qaapi/qa/Makefile
new file mode 100644 (file)
index 0000000..4510709
--- /dev/null
@@ -0,0 +1,8 @@
+
+MODULES_DIR := Tests
+QA_MODULES := plc api
+
+inits := $(foreach module, $(QA_MODULES), $(module)-init)
+
+
+
diff --git a/qaapi/qa/QAAPI.py b/qaapi/qa/QAAPI.py
new file mode 100644 (file)
index 0000000..5310895
--- /dev/null
@@ -0,0 +1,90 @@
+import sys, os
+import traceback
+import qa.modules
+from qa.Config import Config
+from qa.logger import log      
+
+class QAAPI:
+   
+    modules_path = os.path.realpath(qa.modules.__path__[0])
+    methods = []
+               
+       
+    def __init__(self, globals = globals(), config = None, logging=None):
+       if config is None: self.config = Config()
+       else: self.config = Config(config)
+
+       # Load methods
+       real_files = lambda name: not name.startswith('__init__') \
+                                 and  name.endswith('.py')
+       remove_ext = lambda name: name.split(".py")[0]  
+       iterator = os.walk(self.modules_path)
+       (root, basenames, files) = iterator.next()
+       method_base = ""
+       self.methods.extend([method_base+file for file in map(remove_ext, filter(real_files, files))])  
+       for (root, dirs, files) in iterator:
+           parts = root.split(os.sep)  
+           for basename in basenames:
+               if basename in parts:
+                   method_base = ".".join(parts[parts.index(basename):])+"."
+           files = filter(real_files, files)
+           files = map(remove_ext, files)      
+           self.methods.extend([method_base+file for file in  files]) 
+
+       # Add methods to self and global environment 
+        for method in self.methods:
+           callable = self.callable(method)(self.config)
+           if logging: callable = log(callable, method)
+           elif hasattr(self.config, 'log') and self.config.log:
+                callable = log(callable, method)
+           
+           class Dummy: pass
+            paths = method.split(".")
+            if len(paths) > 1:
+                first = paths.pop(0)
+
+                if not hasattr(self, first):
+                    obj = Dummy()
+                    setattr(self, first, obj)
+                    # Also add to global environment if specified
+                    if globals is not None:
+                        globals[first] = obj
+
+                obj = getattr(self, first)
+
+                for path in paths:
+                    if not hasattr(obj, path):
+                        if path == paths[-1]:
+                            setattr(obj, path, callable)
+                           globals[method]=obj  
+                        else:
+                            setattr(obj, path, Dummy())
+                    obj = getattr(obj, path)
+           else:
+               if not hasattr(self, method):
+                   setattr(self, method, callable)
+               if globals is not None:
+                   globals[method] = callable          
+               
+
+    def callable(self, method):
+       """
+       Return a new instance of the specified method. 
+       """      
+        
+       # Look up test  
+       if method not in self.methods:
+           raise Exception, "Invalid method: %s" % method
+
+       # Get new instance of method
+       try:
+           #classname = method.split(".")[-1]
+           module_name = "qa.modules."+method
+           module = __import__(module_name, globals(), locals(), module_name)
+           components = module_name.split('.')
+           module = getattr(module, components[-1:][0])        
+           return module
+       except ImportError, AttributeError:
+           raise  
+        
+                
diff --git a/qaapi/qa/Test.py b/qaapi/qa/Test.py
new file mode 100644 (file)
index 0000000..9a80345
--- /dev/null
@@ -0,0 +1,68 @@
+import utils
+import sys, os
+from types import *
+from logger import log
+
+
+class Test:
+    """
+    Base class for all QA test functions. At a minimum all tests
+    must define:
+
+    call(arg1, arg2, ...): method body
+    """
+
+    accepts = []
+    returns = bool
+
+
+    def call(self, *args):
+       """
+       Method body for test functions. Must override.
+       """
+       return True
+
+    def __init__(self, config):
+       self.name = self.__class__.__name__
+       self.path=os.path.dirname(sys.argv[0])
+       self.config = config
+       self.errors = []
+
+
+    def __call__(self, *args, **kwds):
+       """
+       Main entry point for test functions. logs methods
+       """
+
+       #(min_args, max_args, defaults) = self.args()
+       
+       # Check that the right number of arguments were passed in
+        #if len(args) < len(min_args) or len(args) > len(max_args):
+        #    raise Exception#, (len(args), len(min_args), len(max_args))
+               
+       result = self.call(*args, **kwds)
+       return result   
+           
+
+    def args(self):
+        """
+        Returns a tuple:
+
+        ((arg1_name, arg2_name, ...),
+         (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
+         (None, None, ..., optional1_default, optional2_default, ...))
+
+        That represents the minimum and maximum sets of arguments that
+        this function accepts and the defaults for the optional arguments.
+        """
+
+        # Inspect call. Remove self from the argument list.
+        max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
+        defaults = self.call.func_defaults
+        if defaults is None:
+            defaults = ()
+
+        min_args = max_args[0:len(max_args) - len(defaults)]
+        defaults = tuple([None for arg in min_args]) + defaults
+
+        return (min_args, max_args, defaults)
diff --git a/qaapi/qa/__init__.py b/qaapi/qa/__init__.py
new file mode 100644 (file)
index 0000000..5408724
--- /dev/null
@@ -0,0 +1,3 @@
+all="""
+Test
+"""
diff --git a/qaapi/qa/logger.py b/qaapi/qa/logger.py
new file mode 100644 (file)
index 0000000..1f88eca
--- /dev/null
@@ -0,0 +1,63 @@
+import os
+import traceback
+import time
+
+class Logfile:
+    """
+    Represents a logfile. Used to write data to a file. e.g.
+
+       logfile = Logfile('filename')
+        print >> logfile, data         
+    """                
+    def __init__(self, filename):
+        self.filename = filename
+
+    def write(self, data):
+        try:
+            fd = os.open(self.filename, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0644)
+            os.write(fd, '%s' % data)
+            os.close(fd)
+        except OSError:
+            sys.stderr.write(data)
+            sys.stderr.flush()
+
+
+
+def log(method, method_name = None, \
+       log_filename = 'system.log', errorlog_filename = 'system_error.log'):
+    """
+    Logs whether the specified method completed successfully or not and 
+    returns the method result. Use as a decorator, e.g.,
+       
+        @log
+       def foo(...):
+           ...
+       
+    Or:
+       def foo(...):
+           ...
+       foo = log(foo)
+
+    Or:
+       result = log(foo)(...)                  
+
+    """
+    logfile = Logfile(log_filename)
+    error_logfile = Logfile(errorlog_filename)                         
+    
+    if method_name is None:
+       method_name = method.__name__
+    
+    def wrapper(*args, **kwds):
+        
+       print >> logfile, method_name + ": ",
+        try:
+           #print >> logfile, args, 
+            result = method(*args, **kwds)
+            print >> logfile, " [OK]"
+        except:
+            print >>logfile, " [FAILED]"
+            print >> error_logfile, "%s: %s\n" % (method_name, traceback.format_exc())
+           raise
+
+    return wrapper
diff --git a/qaapi/qa/modules/__init__.py b/qaapi/qa/modules/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qaapi/qa/modules/api/__init__.py b/qaapi/qa/modules/api/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qaapi/qa/modules/plc/__init__.py b/qaapi/qa/modules/plc/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qaapi/qa/modules/plc/configure.py b/qaapi/qa/modules/plc/configure.py
new file mode 100644 (file)
index 0000000..6d50303
--- /dev/null
@@ -0,0 +1,37 @@
+
+import os, sys
+import traceback
+from qa.Test import Test
+
+class configure(Test):
+    """
+    Configure the myplc from config options in config file
+    """
+
+    def call(self):
+       tmpname = '/tmp/plc-cinfig-tty-%d' % os.getpid()
+       fileconf = open(tmpname, 'w')
+       for var in [ 'PLC_NAME',
+                     'PLC_ROOT_PASSWORD',
+                     'PLC_ROOT_USER',
+                     'PLC_MAIL_ENABLED',
+                     'PLC_MAIL_SUPPORT_ADDRESS',
+                     'PLC_DB_HOST',
+                     'PLC_API_HOST',
+                     'PLC_WWW_HOST',
+                     'PLC_BOOT_HOST',
+                     'PLC_NET_DNS1',
+                     'PLC_NET_DNS2']:
+           fileconf.write('e %s\n%s\n' % (var, self.config[var]))
+       fileconf.write('w\nq\n')
+       fileconf.close()
+
+        # set configuration options
+       if self.config.verbose: os.system('set -x ; cat %s'%tmpname)
+        (stdin, stdout, stderr) = \
+            os.popen3('set -x ; chroot /plc/root  plc-config-tty < %s'%tmpname)
+        self.errors = stderr.readlines()
+        if self.errors: raise "\n".join(self.errors)
+        os.system('set -x; rm %s'%tmpname)
+
+       return 1
diff --git a/qaapi/qa/modules/plc/install.py b/qaapi/qa/modules/plc/install.py
new file mode 100644 (file)
index 0000000..679ca28
--- /dev/null
@@ -0,0 +1,34 @@
+
+import os, sys
+import traceback
+from qa.Test import Test
+
+
+class install(Test):
+    """
+    Installs a myplc
+    """
+
+    def call(self, url=None):
+       
+       # Determine url 
+       if not url:
+           url_file = open("%s/URL" % self.path)
+           url = url_file.read().strip()
+           url_file.close()
+       
+       # Save url
+       if self.config.verbose:
+           utils.header('Saving current myplc url into %s/URL' % self.path)
+       fsave=open('%s/URL' % self.path, "w")
+       fsave.write(url+'\n')
+       fsave.close()
+       
+       # Instal myplc from url          
+       if self.config.verbose:
+           utils.header('Installing myplc from url %s' % url)
+        (stdin, stdout, stderr) = os.popen3('set -x; rpm -Uvh ' + self.url)
+        self.errors = stderr.readlines()
+        if self.errors: raise "\n".join(self.errors)   
+       
+       return 1
diff --git a/qaapi/qa/modules/plc/start.py b/qaapi/qa/modules/plc/start.py
new file mode 100644 (file)
index 0000000..97c3c75
--- /dev/null
@@ -0,0 +1,15 @@
+import os, sys
+import traceback
+from qa.Test import Test
+
+class start(Test):
+    """
+    Installs a myplc
+    """
+
+    def call(self):
+       (stdin, stdout, stderr) = os.popen3('set -x ; service plc start')
+        self.errors = stderr.readlines()
+        if self.errors: raise "\n".join(self.errors)
+
+       return 1
diff --git a/qaapi/qa/modules/plc/stop.py b/qaapi/qa/modules/plc/stop.py
new file mode 100644 (file)
index 0000000..9e250ca
--- /dev/null
@@ -0,0 +1,15 @@
+import os, sys
+import traceback
+from qa.Test import Test
+
+class stop(Test):
+    """
+    Installs a myplc
+    """
+
+    def call(self):
+       (stdin, stdout, stderr) = os.popen3('set -x ; service plc stop')
+        self.errors = stderr.readlines()
+        if self.errors: raise "\n".join(self.errors)
+
+       return 1
diff --git a/qaapi/qa/modules/plc/uninstall.py b/qaapi/qa/modules/plc/uninstall.py
new file mode 100644 (file)
index 0000000..fdca683
--- /dev/null
@@ -0,0 +1,23 @@
+import os, sys
+import traceback
+from qa.Test import Test
+from qa import utils
+
+class uninstall(Test):
+    """
+    Completely removes the installed myplc
+    """
+
+    def call(self):
+       if self.config.verbose:
+            utils.header("Removing myplc")
+        (stdin, stdout, stderr) = os.popen3('set -x; service plc safestop')
+        self.errors = stderr.readlines()
+        (stdin, stdout, stderr) = os.popen3('set -x; rpm -e myplc')
+        self.errors.extend(stderr.readlines())
+        if self.errors: raise "\n".join(self.errors)
+        (stdin, stdout, stderr) = os.popen3('set -x; rm -rf  /plc/data')
+        self.errors.extend(stderr.readlines())
+        if self.errors: raise "\n".join(self.errors)
+       
+       return 1
diff --git a/qaapi/qa/qa_config b/qaapi/qa/qa_config
new file mode 100644 (file)
index 0000000..61d1c2f
--- /dev/null
@@ -0,0 +1,36 @@
+# system test config file
+
+# DISPLAY VMPlayer will use
+DISPLAY=":0.0"
+
+HOSTNAME="localhost"
+
+DOMAIN_NAME="localhost.localdomain"
+
+#PLC_ROOT_USER="root@localhost.localdomain"
+PLC_ROOT_USER="root@localhost.localdomain"
+#PLC_ROOT_PASSWORD="password"
+PLC_ROOT_PASSWORD="root"
+
+PLC_NAME="TestLab"
+
+PLC_API_HOST="localhost.localdomain"
+
+PLC_DB_HOST="localhost.localdomain"
+
+PLC_WWW_HOST="localhost.localdomain"
+
+PLC_BOOT_HOST="localhost.localdomain"
+
+PLC_MAIL_ENABLED=1
+
+PLC_MAIL_SUPPORT_ADDRESS="root@localhost.localdomain"
+
+PLC_NET_DNS1="138.96.0.10"
+
+PLC_NET_DNS2="138.96.0.11"
+
+NODE_1_HOSTNAME="hostname"
+
+NODE_2_HOSTNAME="hostname"
diff --git a/qaapi/qa/tests/__init__.py b/qaapi/qa/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qaapi/qa/utils.py b/qaapi/qa/utils.py
new file mode 100644 (file)
index 0000000..971ff52
--- /dev/null
@@ -0,0 +1,7 @@
+# $Id$
+import time
+
+# how could this accept a list again ?
+def header(message):
+    now=time.strftime("%H:%M:%S", time.localtime())
+    print "*",now,'--',message
diff --git a/qaapi/qash b/qaapi/qash
new file mode 100755 (executable)
index 0000000..3eae64f
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+#
+# Interactive shell for using QA test suite
+
+
+
+import os, sys
+from optparse import OptionParser
+from traceback import print_exc
+
+import utils
+import System
+from qa.QAAPI import QAAPI
+
+sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0])))
+
+parser = OptionParser(add_help_option = False)
+parser.add_option("-f", "--config", help = "configuration file")
+parser.add_option("-l", "--log", help="enable logging")
+parser.add_option("--help", action = "help", help = "show this help message and exit")
+(options, args) = parser.parse_args()
+
+# XX Load callable tests
+try:
+   qaapi import QAAPI(globals(), 
+                     options.config, 
+                     options.log)
+
+except Exception, err:
+    print "Error: ", err
+    parser.print_help()
+    sys.exit(1)
+
+# If called by a script
+if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
+    # Pop us off the argument stack
+    sys.argv.pop(0)
+    execfile(sys.argv[0])
+
+else:
+    prompt = "[QA]"
+    
+     # Readline and tab completion support
+    import atexit
+    import readline
+    import rlcompleter
+
+    print 'Type "help" for more information.'
+    # Load command history
+    history_path = os.path.join(os.environ["HOME"], ".qa_history")
+    try:
+        file(history_path, 'a').close()
+        readline.read_history_file(history_path)
+        atexit.register(readline.write_history_file, history_path)
+    except IOError:
+        pass
+
+    # Enable tab completion
+    readline.parse_and_bind("tab: complete")
+
+    try:
+        while True:
+            command = ""
+            while True:
+                # Get line
+                try:
+                    if command == "":
+                        sep = ">>> "
+                    else:
+                        sep = "... "
+                    line = raw_input(prompt + sep)
+                # Ctrl-C
+                except KeyboardInterrupt:
+                    command = ""
+                    print
+                    break
+
+                # Build up multi-line command
+                command += line
+
+                # Blank line or first line does not end in :
+                if line == "" or (command == line and line[-1] != ':'):
+                    break
+
+                command += os.linesep
+
+           # Blank line
+            if command == "":
+                continue
+            # Quit
+            elif command in ["q", "quit", "exit"]:
+                break
+
+            try:
+                try:
+                    # Try evaluating as an expression and printing the result
+                    result = eval(command)
+                    if result is not None:
+                        print result
+                except SyntaxError:
+                    # Fall back to executing as a statement
+                    exec command
+            except Exception, err:
+                print_exc()
+
+    except EOFError:
+        print
+        pass   
diff --git a/qaapi/setup.py b/qaapi/setup.py
new file mode 100755 (executable)
index 0000000..91da055
--- /dev/null
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+#
+# Setup script for PLCAPI
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: setup.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+import os
+from distutils.core import setup
+from glob import glob
+
+path = os.getcwd()
+name = 'qa'
+path = path+os.sep+name
+pkgs = [path]
+
+iterator = os.walk(path)
+for (root, dirs, files) in iterator:
+    pkgs.extend([root+os.sep+d for d in dirs])                 
+
+print pkgs 
+setup(name = name,
+      packages = pkgs,
+      scripts = ['qash']
+                    )