[libvirt] [PATCH 1/2 v3] Python: Refactoring virTypedParameter conversion for NUMA tuning APIs

Eric Blake eblake at redhat.com
Thu Feb 2 17:48:33 UTC 2012


On 01/28/2012 07:53 AM, Guannan Ren wrote:
>         *virDomainSetNumaParameters
>         *virDomainGetNumaParameters
> ---
>  python/Makefile.am              |    4 +-
>  python/libvirt-override-api.xml |   13 ++
>  python/libvirt-override.c       |  314 +++++++++++++++++++++++++++++++++++++++
>  3 files changed, 330 insertions(+), 1 deletions(-)
> 
> diff --git a/python/Makefile.am b/python/Makefile.am
> index 3068eee..4302fa5 100644
> --- a/python/Makefile.am
> +++ b/python/Makefile.am
> @@ -8,6 +8,8 @@ SUBDIRS= . tests
>  INCLUDES = \
>  	$(PYTHON_INCLUDES) \
>  	-I$(top_srcdir)/include \
> +        -I$(top_srcdir)/src \
> +        -I$(top_srcdir)/gnulib/lib \

Hmm, you converted TAB to space.

>  	-I$(top_builddir)/include \

Also, since gnulib has some directly-supplied headers (srcdir) and some
generated headers (builddir), we really should be using both locations
so as not to break VPATH.

>  	-I$(top_builddir)/$(subdir) \
>  	$(GETTEXT_CPPFLAGS)
> @@ -42,7 +44,7 @@ all-local: libvirt.py libvirt_qemu.py
>  
>  pyexec_LTLIBRARIES = libvirtmod.la libvirtmod_qemu.la
>  
> -libvirtmod_la_SOURCES = libvirt-override.c typewrappers.c
> +libvirtmod_la_SOURCES = libvirt-override.c typewrappers.c ../src/util/virtypedparam.c

I'm not sure I like this.  Rather than pulling in just one or two source
files, we should probably instead figure out how to directly link
against the libvirt_util library and have all of the functions
available.  This would also make it possible to use VIR_FREE and friends
(at which point, we should disable the syntax-check exceptions currently
in effect on the python files).

I think I will do a preliminary patch, which does _just_ the makefile
work to pull in the use of libvirt_util, then we can rebase this patch
on top of that one.  I know Alex Jia was also complaining about the
inability to use normal libvirt conventions, because the Makefile wasn't
yet set up for it, so this will be a good move overall.

> +    <function name='virDomainSetNumaParameters' file='python'>
> +      <info>Change the NUMA tunables</info>
> +      <return type='int' info='-1 in case of error, 0 in case of success.'/>
> +      <arg name='domain' type='virDomainPtr' info='pointer to domain object'/>
> +      <arg name='params' type='virTypedParameterPtr' info='pointer to numa tunable objects'/>

Is th is type correct, or can it be any python dictionary type that maps
valid numa tunable parameter names to values?

> +      <arg name='flags'  type='int' info='an OR'ed set of virDomainModificationImpact'/>
> +    </function>
> +    <function name='virDomainGetNumaParameters' file='python'>
> +      <info>Get the NUMA parameters</info>
> +      <return type='int' info='returns a dictionary of params in case of success, -1 in case of error'/>

The return type should be a python object - a dictionary on success,
PyNone on failure where libvirt populated an error message, or NULL on a
python exception.

> +++ b/python/libvirt-override.c
> @@ -21,6 +21,7 @@
>  #include "libvirt/virterror.h"
>  #include "typewrappers.h"
>  #include "libvirt.h"
> +#include "util/virtypedparam.h"

Hmm, the rest of our code sets up INCLUDES so that we can use just
"virtypedparam.h" instead of "util/virtypedparam.h"; another thing for
me to do in pulling out the infrastructure into a preliminary patch.

>  
>  #ifndef __CYGWIN__
>  extern void initlibvirtmod(void);
> @@ -61,6 +62,208 @@ static char *py_str(PyObject *obj)
>      return PyString_AsString(str);
>  }
>  
> +/* Two helper functions to help the conversions between C to Python
> + * for the virTypedParameter used in the following APIs. */
> +static PyObject *
> +getPyVirTypedParameter(virTypedParameterPtr params, int nparams)
> +{
> +    PyObject *info;
> +    PyObject *key, *val;
> +    PyObject *ret = NULL;
> +    int i;
> +
> +    if (!params)
> +        return ret;

If we return NULL, we should ensure that there is a valid python
exception object ready for the caller to access.  I'm thinking it might
be better to mark this function with ATTRIBUTE_NONNULL(1) to avoid
worrying about whether the caller has properly generated a python
exception before passing us NULL.

> +
> +    /* convert to a Python tuple of long objects */
> +    if ((info = PyDict_New()) == NULL) {
> +        return ret;
> +    }

This one's fine - PyDict_New guarantees a python exception is ready to go.

> +
> +    for (i = 0 ; i < nparams ; i++) {
> +        switch (params[i].type) {
> +        case VIR_TYPED_PARAM_INT:
> +            val = PyInt_FromLong((long)params[i].value.i);
> +            break;

> +
> +        case VIR_TYPED_PARAM_STRING:
> +            val = libvirt_constcharPtrWrap(params[i].value.s);
> +            break;
> +
> +        default:
> +            Py_DECREF(info);
> +            return ret;

This returns NULL but without setting a python exception.  Then again,
this can only happen if the server sent us something which we don't
understand (arguably a bug in the libvirt.so used by the server).  Maybe
it would be better to return Py_None instead of NULL in this situation,
since I don't know what python exception we would raise.

At any rate, if my understanding is correct, your use of a simple
Py_DECREF of a non-empty python dictionary sets up garbage collection
for all the contents of that dictionary (that is, we don't have to
explicitly clean up the contents, just abandoning info was good enough).

> +        }
> +
> +        key = libvirt_constcharPtrWrap(params[i].field);
> +        if (!key || !val)
> +            goto fail;
> +
> +        if (PyDict_SetItem(info, key, val) < 0)
> +            goto fail;
> +
> +        Py_DECREF(key);
> +        Py_DECREF(val);
> +    }
> +    return info;
> +fail:
> +    Py_XDECREF(info);
> +    Py_XDECREF(key);
> +    Py_XDECREF(val);
> +    return ret;
> +}

The rest of this function looks right to me - any way that gets to the
fail label will properly have set a python exception and return NULL.

> +
> +static PyObject *
> +setPyVirTypedParameter(PyObject *info, virTypedParameterPtr params, int nparams)

A comment would go a long way on describing what this function does.  My
understanding is that it takes an incoming python object 'info', which
should behave like a dictionary containing just name/value references,
as well as incoming params previously collected from the domain.  Then
for every key in params found in info, we update params in-place to
contain the new value from info, so that we can then pass params back to
libvirt to update values.

I think this has a couple of design issues.  Consider
virDomainSetBlockIoTune, which currently has 6 defined parameters when
getting values, but where the parameters are, to some degree, mutually
exclusive.  That is, if I set VIR_DOMAIN_BLOCK_IOTUNE_TOTAL_BYTES_SEC to
non-zero, then I must also omit VIR_DOMAIN_BLOCK_IOTUNE_READ_BYTES_SEC
or else set it to 0 when setting values.  But if read_bytes_sec was
previously set, and the user passes in a python array containing just a
mapping for a non-zero total_bytes_sec, then modifying params in place
and passing back all 6 parameters will cause an error (we're passing in
conflicting non-zero values), while passing in a params of just length 1
will properly let libvirt recognize that I am replacing read_bytes_sec
with total_bytes_sec (well, it will once I fix the bugs I just noticed
in qemu_driver.c in handling that situation).  The python code should
mirror the C calling conventions where I can pass in a smaller array
than what I get back out on queries, so that libvirt is free to reuse or
replace unspecified parameters as it sees fit.

In other words, I think we really want a different signature - we want
to pass in three items - a python dictionary, and a C array and length
that tell us what types the code is expecting, and in return get one
item - a new C array with length given by PyDict_Size of the incoming
dictionary, or NULL if a python exception has been raised.  Something
like this signature:

/* Allocate a new typed parameter array with the same contents and
 * length as info, and using the array params of length nparams as
 * hints on what types to use when creating the new array.  Return
 * NULL on failure. */
static virTypedParamterPtr
setPyVirTypedParameter(PyObject *info,
   const virTypedParameterPtr params,
   int nparams)

> +{
> +    PyObject *key, *val;
> +    PyObject *ret = NULL;
> +    int i;
> +
> +    if (!info || !params)
> +        return ret;

Not so good.  We are returning NULL without setting a python exception.
 Again an argument that these should be ATTRIBUTE_NONNULL, and that the
caller should not be passing invalid data.

> +
> +    /* convert to a Python tuple of long objects */

Comment is wrong.  We are converting a python dictionary into a C struct
virTypedParameter array.

> +    for (i = 0; i < nparams; i++) {

I think we're iterating over the wrong struct.  We want to convert ever
object in info into a new C structure, and fail if we don't know how to
do the conversion.  That is, I think we need the overall structure of
this function to look more like:

pos = 0;
allocate return value according to size of info
while (PyDict_Next(info, &pos, &key, &value)) {
    for (i = 0; i < nparams; i++) {
        if params[i].field matches key, use params[i].type and break
    }
    if i == nparams, error out that we don't know how to convert key
    populate ret[pos] according to learned type
}

> +        key = libvirt_constcharPtrWrap(params[i].field);

Need to check for NULL return, and error out.

> +        val = PyDict_GetItem(info, key);
> +        Py_DECREF(key);
> +
> +        if (val == NULL)
> +            continue;

This says that a parameter in params but not in the dictionary is left
unmodified, but it doesn't filter out entries in the dictionary but not
in params.  Again evidence that we are iterating over the wrong struct.

> +
> +        switch (params[i].type) {
> +        case VIR_TYPED_PARAM_INT:
> +            {
> +                long long_val;
> +                if (PyInt_Check(val)) {
> +                    long_val = PyInt_AsLong(val);
> +                    if ((long_val == -1) && PyErr_Occurred())
> +                        return ret;
> +                } else {
> +                    PyErr_SetString(PyExc_TypeError,
> +                                    "Attribute value type must be int");
> +                    return ret;
> +                }
> +                params[i].value.i = (int)long_val;

This allows silent overflow.  We should validate that (int)long_val ==
long_val, and reject it if it is out of range.

> +        case VIR_TYPED_PARAM_STRING:
> +            {
> +                if (PyString_Check(val)) {
> +                    free(params[i].value.s);
> +                    params[i].value.s = PyString_AsString(val);

This can lead to some problematic allocation patterns, since the caller
must not free this string, but you are replacing a string that the
caller must otherwise free to avoid a leak.  All the more reason you
need to return a new virTypedParameterPtr array, and not modify the
incoming one in place.

You are missing a check for a NULL return; but then again, the previous
call to PyString_Check guarantees that you should succeed.  On the other
hand, since PyString_AsString properly raises a python exception if val
is not a string, we could skip the PyString_Check() call.  I don't know
which way is easier to write.

> +                } else {
> +                    PyErr_SetString(PyExc_TypeError,
> +                                    "Attribute value type must be string");
> +                    return ret;
> +                }
> +            }
> +            break;
> +        default:
> +            return ret;
> +        }
> +    }
> +    return VIR_PY_NONE;

Here, I'd return the new array that we just created, or NULL; no need to
return VIR_PY_NONE since the result of this function is not passed back
to python, but only used in calling a libvirt C function.

>  
>  static PyObject *
> +libvirt_virDomainSetNumaParameters(PyObject *self ATTRIBUTE_UNUSED,
> +                                     PyObject *args)

Indentation.

> +    if (!setPyVirTypedParameter(info, params, nparams))
> +        goto fail;

Given my above thoughts, you should be storing the result of this call,

> +
> +    LIBVIRT_BEGIN_ALLOW_THREADS;
> +    i_retval = virDomainSetNumaParameters(domain, params, nparams, flags);

and passing that result, along with the size of info (and _not_ nparams).

> +    LIBVIRT_END_ALLOW_THREADS;
> +
> +    if (i_retval < 0) {
> +        ret = VIR_PY_INT_FAIL;
> +        goto fail;
> +    }
> +
> +    /* The string generated by PyString_AsString
> +     * must not be deallocated */

Ah, but if you follow my advice about setPyVirTypedParameter returning a
_new_ array, then what we really want to do here is:

virTypedParameterArrayClear(params, nparams);
VIR_FREE(params);
VIR_FREE(new_params);

That is, params contains strings returned by libvirt which the caller
must free, while new_params contains strings which point into python
objects and must not be freed.

> +    free(params);
> +    return VIR_PY_INT_SUCCESS;
> +fail:
> +    /*same as above*/
> +    free(params);
> +    return ret;

We should probably combine these cleanups into a single path, using our
common idiom:

    ret = VIR_PY_INT_SUCCESS;
cleanup:
    free params...
    return ret;

and change goto fail into goto cleanup.

> +}
> +
> +static PyObject *
> +libvirt_virDomainGetNumaParameters(PyObject *self ATTRIBUTE_UNUSED,
> +                                     PyObject *args)
> +{

> +
> +    if (i_retval < 0)
> +        return VIR_PY_INT_FAIL;

Wrong return value - here, you want to return None, since the successful
return is a python dictionary and not the integer 0.

You might want to take a shortcut here - if nparams is 0, you can return
an empty dictionary, rather than continuing on with mallocing a 0-length
array and wasting time with more libvirt calls.

> +
> +    if ((params = malloc(sizeof(*params)*nparams)) == NULL)
> +        return PyErr_NoMemory();
> +
> +    LIBVIRT_BEGIN_ALLOW_THREADS;
> +    i_retval = virDomainGetNumaParameters(domain, params, &nparams, flags);
> +    LIBVIRT_END_ALLOW_THREADS;
> +
> +    if (i_retval < 0) {
> +        ret = VIR_PY_INT_FAIL;

Again, this should return None, not -1.

-- 
Eric Blake   eblake at redhat.com    +1-919-301-3266
Libvirt virtualization library http://libvirt.org

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 620 bytes
Desc: OpenPGP digital signature
URL: <http://listman.redhat.com/archives/libvir-list/attachments/20120202/eab097bb/attachment-0001.sig>


More information about the libvir-list mailing list