Tue, Nov 29, 2016 at 04:45:41PM CET, olichtne@redhat.com wrote:
Update after todays meeting, contains some changes and some notes to think about, probably best to just diff it against the previous version...
I'm a bit stolen from LNST lately, so please take that into consideration while reading my comments :) I'll try to look not that dumb :)
===============================================================================
- test modules
class BaseTestModule: def __init__(self, **kwargs): #by defaults loads the params into self.params - no checks pseudocode: for x in vars(self): if isinstance(x, BaseType): param_class = self.getattr(x) try: val = kwargs[x] except KeyError: if param_class.is_mandatory(): raise TestModuleError("Option x is mandatory") self.setattr(x.params, param_class.construct(val)) del kwargs[x] for x in kwargs.keys(): log.error("Undefined parameter x") if len(kwargs): raise TestModuleError("Undefined TestModule parameters")
def run(): #needs to be over-ridden - throw an exception to notify the test developer
class MyTest(BaseTestModule): param = ParamType(mandatory=True) param2 = ParamType2() param3 = Multiparam(ParamType())
#optional __init__ #def __init__(self, **kwargs): #super(MyTest).__init__(kwargs) #additional tester defined checks
def run(): #do my test #parameters available in self.params
#in Task: import lnst #module lnst.tests will dynamically look for module classes in configured #locations, similar to how we do it now
ping = lnst.modules.Ping(dst=m2.if1.ip[0], count=100, interval=0.1)
m1.run(ping)
I would still like to have one-shot possibility. Most of the time, this is 2 phase thing. You create test module instance and you call run. I would like to have possibility to just run. Just a thought...
================================================
- Tasks:
class BaseTask(object): def __init__(self): #initialize instance specific requirements self.requirements = Requirements() for x in dir(self): val = getattr(self, x) if instance(val, Requirement): setattr(self.requirements, x, val)
self.params = object() for x in dir(self): val = getattr(self, x) if instance(val, ParamType): setattr(self.params, x, val)
def test(): raise Exception("Method test MUST be defined.")
class MyTask(lnst.BaseTask): #class-wide definition of requirements m1 = HostReq(arch="x86_64", ...) m1.if1 = IfaceReq(label="xyz", driver="mlx", ...)
#m1.params = IfaceReq() #m1.devs = IfaceReq() #raise Exception("param/devs name is a reserved keyword")
m2 = HostReq(param="val", ...) m2.if1 = IfaceReq(label="xyz", param="val", ...)
mtu = IntType(default=1500)
So I understand this IntType and the "ParamType*" used in the test module are the same classes? If not, I think they should, if possible.
def __init__(self, **kwargs): super(self, lnst.BaseTask).__init__()
#do something with kwargs interface_driver = kwargs["driver"] self.reqs.m1.if1.driver = interface_driver #adjust instance specific requirements self.reqs.m3 = HostReq(...)
def test(): self.matched.m1.run(Module) self.matched.m1.run("command")
Hmm, thinking about it, I probably like this first option better. Any thoughts about this?
#or def test(m1, m2, m3, m4,...):
Yeah, looks a bit messy...
m1.run(Module) m2.run("command") m1.params.arch == "x86_64"
#incorrect... def test(m2, test_machine1)
================================================
- Running Tasks:
my_test_script.py: #!/bin/python from MyTasks import MyTask from lnst import Controller
task_instance = MyTask(mtu="5000")
ctl = Controller(config="/etc/lnst-ctl.conf") ctl.run(task_instance)
chmod +x my_test_script.py ./my_test_script.py args
Hmm, who parses the args? Shouldn't you pass them to Controller or MyTask?
I have similar concern here as for the test modules you always do 2 step: ctl = Controller(config="/etc/lnst-ctl.conf") ctl.run(task_instance)
Can this be done in a nicer way? One shot?
OR
lnst-ctl -d run MyTask.py -- mtu=8000 # looks for NAME class in the NAME.py file (MyTask in this case for which # the condition "isinstance(NAME, BaseTask)" must be True
# could also run for all classes in the file where "isinstance(x, BaseTask)" is # True. with the option to restrict to specific task class (or just run the # first one?)... lnst-ctl rewritten to do the same as manually running the
Not just the first one. Run all.
# task from it's own python script
Aliases lose meaning - they're parameters passed to the MyTask __init__, when using the lnst-ctl CLI, use "-- task_params"?? might not work for multiple tasks,
That is fine. I hated aliases anyway. These are options per task. We can comeup with same standard options and define them somewhere, so they doen't get re-invented for every new task.
================================================
- Tester facing API, inside the test() method:
#!!!! breakpoints!!!
Host objects available in self.matched.selector_name:
class Host: #HostAPI??? name can change
No "API" in class name please! That is just horrible.
#attributes:
# dynamically filled object of Host attributes such as architecture and # so on. Use example in test() would look like this: # if host.params.arch == "x86":
Calling this a "param" sounds odd...
# I separated this into the "params" object so I can overwrite its # __getattr__ method and return None/UnknownParam exception for unknown # parameters, and to avoid name conflicts with other attributes # they should also be iterable by: # for param in host.params: params = object()
# dynamically filled object of NetDevice objects accessible directly as the # object attributes: # host.eth0.set_ip(...)
I think we need to do a list of any possible setter and getter, think about how to unify the name and behaviour of most.
# I separated this into the "ifaces" object to avoid name conflicts with # other attributes # creation of new NetDevices should be possible through simple assignement: # m1.team0 = TeamDevice(...) # assignement of an incompatible Type or to an existing Device object will # return an exception # to deconfigure+remove call m1.team0.remove() __devs = object()
# device iterator for the Host object: # for dev in host:
def run(what, bg=False, fail=False, timeout=60, path="", json=False, netns) # will run "what" on the remote host # "what" is either a Module object, or a string command that will be # executed as a bash command # "bg" when True, runs "what" on background - the run() call # immediately returns, and "timeout" is ignored, the background # process can be controlled through the returned Job object # "fail" if True then the Job is expected to fail, and will be reported # as PASSed if it does # "timeout" in seconds, determines how long to block test execution for # before killing the Job. Only when running in foreground # "path" changes the current working directory to the specified path # before "what" is executed and changes back after execution is # finished. # IGNORE FOR NOW # "tool" changes the current working directory to the directory of a # speficied test_tool before "what" is executed and changes back # after execution is finished. # !!!!!!! this is from the current API and i'm not yet sure how we # !!!!!!! want to handle those... so for now I'll keep it # IGNORE FOR NOW
Yeah, just remove this weirdness...
# "json" if True will attempt to parse the returned stdout of the Job # as json into a dictionary # "netns" Job will be run in the specified network namespace # Returns a Job object # !! Exceptions?!?! # !!!THINK ABOUT THESE...
Any news in this area?
def __set(path, value) # copied from old API, provides a shortcut for "echo $value # >/proc/or/sys/path" # and returns the original value when the test is finished
def sysfs_set(path, value): # check if path starts with "/sys"? self.__set(path, value)
def procfs_set(path, value): self.__set("/proc/"+path, value)
def send_file(ctl_path="", slave_path="", recursive=False) # copies the specified file from the controller to the specified # destination path, if recursive == True and srcpath refers to a # directory it copies the entire directory
def recv_file(recv_path="", ctl_path="", recursive=False)
def {enable, disable}_service(service) # copied from old API, enables or disables the specified service
def del_device(name) # removes the specified device, probably easier (more logical?) to do # this then "devs.name = None" and "del devs.name" would be unreliable # !!! REMOVE
class Device: #DeviceAPI, InterfaceAPI? name can change... # attributes:
# dynamically created Device attributes such as driver and so on. Use # example in test() would look like this: # if host.devs.eth0.driver == "ixgbe": # achieved through rewriting of the __getattr__ method of the Device class # should return None or throw UnknownParam exception for unknown parameters # this should directly mirror the Device objects that are managed by the # InterfaceManager on the Slave # !!! predefine the params, assignment will set them, sync on set - wait # !!! on the slave for the set to finish and return the current status of # !!! everything else as well # eg: driver = something mtu = something ips = [IpAddress, ...]
class Job: #ProcessAPI? name can change... #attributes:
# True if the Job finished, False if it's still running in the background finished = bool
# contains the result data returned by the Job, None for bash commands result = object
# contain the stdout and stderr generated by the job, None for Module Jobs stdout = "" stderr = ""
# simple True/False value indicating success/failure of the Job passed = bool
def wait(timeout=0): # for background jobs, will wait until the job finished # "timeout" in seconds, determines how long to wait for. After timeout # reached, nothing happens, status of the job can be checked with the # "finished" attribute. If timeout=0, then wait forever.
def kill(signalnum=signal.SIGKILL): # sends the specified signal to the process of the Job running in # background # "signalnum" the signal to be sent
def __str__(self): # for easy print m1.run(what) return stdout+stderr
Thanks for taking care of this!