[lxc-devel] [PATCH 7/8] python: add attach support

Christian Seiler christian at iwakd.de
Tue Aug 13 21:56:19 UTC 2013


Add methods attach() and attach_wait() to the Python API that give
access to the attach functionality of LXC. Both accept two main
arguments:

1. run: A python function that is executed inside the container
2. payload: (optional) A parameter that will be passed to the python
            function

Additionally, the following keyword arguments are supported:

attach_flags: How attach should operate, i.e. whether to attach to
              cgroups, whether to drop capabilities, etc. The following
              constants are defined as part of the lxc module that may
              be OR'd together for this option:
                LXC_ATTACH_MOVE_TO_CGROUP
                LXC_ATTACH_DROP_CAPABILITIES
                LXC_ATTACH_SET_PERSONALITY
                LXC_ATTACH_APPARMOR
                LXC_ATTACH_REMOUNT_PROC_SYS
                LXC_ATTACH_DEFAULT
namespaces: Which namespaces to attach to, as defined as the flags that
            may be passed to the clone(2) system call. Note: maybe we
            should export these flags too.
personality: The personality of the process, it will be passed to the
             personality(2) syscall. Note: maybe we should provide
             access to the function that converts arch into
             personality.
initial_cwd: The initial working directory after attaching.
uid: The user id after attaching.
gid: The group id after attaching.
env_policy: The environment policy, may be one of:
              LXC_ATTACH_KEEP_ENV
              LXC_ATTACH_CLEAR_ENV
extra_env_vars: A list (or tuple) of environment variables (in the form
                KEY=VALUE) that should be set once attach has
                succeeded.
extra_keep_env: A list (or tuple) of names of environment variables
                that should be kept regardless of policy.
stdin: A file/socket/... object that should be used as stdin for the
       attached process. (If not a standard Python object, it has to
       implemented the fileno() method and provide a fd as the result.)
stdout, stderr: See stdin.

attach() returns the PID of the attached process, or -1 on failure.

attach_wait() returns the return code of the attached process after
that has finished executing, or -1 on failure. Note that if the exit
status of the process is 255, -1 will also be returned, since attach
failures result in an exit code of 255.

Two default run functions are also provided in the lxc module:

attach_run_command: Runs the specified command
attach_run_shell: Runs a shell in the container

Examples (assumeing c is a Container object):

c.attach_wait(lxc.attach_run_command, 'id')
c.attach_wait(lxc.attach_run_shell)
def foo():
  print("Hello World")
  # the following line is important, otherwise the exit code of
  # the attached program will be -1
  # sys.exit(0) will also work
  return 0
c.attach_wait(foo)
c.attach_wait(lxc.attach_run_command, ['cat', '/proc/self/cgroup'])
c.attach_wait(lxc.attach_run_command, ['cat', '/proc/self/cgroup'],
              attach_flags=(lxc.LXC_ATTACH_DEFAULT &
              ~lxc.LXC_ATTACH_MOVE_TO_CGROUP))

Note that while it is possible to execute Python code inside the
container by passing a function (see example), it is unwise to import
modules, since there is no guarantee that the Python installation
inside the container is in any way compatible with that outside of it.
If you want to run Python code directly, please import all modules
before attaching and only use them within the container.

Signed-off-by: Christian Seiler <christian at iwakd.de>
---
 src/python-lxc/lxc.c           |  241 ++++++++++++++++++++++++++++++++++++++++
 src/python-lxc/lxc/__init__.py |   57 ++++++----
 2 files changed, 276 insertions(+), 22 deletions(-)

diff --git a/src/python-lxc/lxc.c b/src/python-lxc/lxc.c
index ec81bbf..ddf3fa0 100644
--- a/src/python-lxc/lxc.c
+++ b/src/python-lxc/lxc.c
@@ -24,6 +24,7 @@
 #include <Python.h>
 #include "structmember.h"
 #include <lxc/lxccontainer.h>
+#include <lxc/utils.h>
 #include <stdio.h>
 #include <sys/wait.h>
 
@@ -697,6 +698,214 @@ Container_wait(Container *self, PyObject *args, PyObject *kwds)
     Py_RETURN_FALSE;
 }
 
+struct lxc_attach_python_payload {
+    PyObject *fn;
+    PyObject *arg;
+};
+
+static int lxc_attach_python_exec(void* _payload)
+{
+    struct lxc_attach_python_payload *payload = (struct lxc_attach_python_payload *)_payload;
+    PyObject *result = PyObject_CallFunctionObjArgs(payload->fn, payload->arg, NULL);
+    
+    if (!result) {
+        PyErr_Print();
+        return -1;
+    }
+    if (PyLong_Check(result))
+        return (int)PyLong_AsLong(result);
+    else
+        return -1;
+}
+
+static lxc_attach_options_t *lxc_attach_parse_options(PyObject *kwds)
+{
+    static char *kwlist[] = {"attach_flags", "namespaces", "personality", "initial_cwd", "uid", "gid", "env_policy", "extra_env_vars", "extra_keep_env", "stdin", "stdout", "stderr", NULL};
+    long temp_uid, temp_gid;
+    int temp_env_policy;
+    PyObject *extra_env_vars_obj = NULL;
+    PyObject *extra_keep_env_obj = NULL;
+    PyObject *stdin_obj = NULL;
+    PyObject *stdout_obj = NULL;
+    PyObject *stderr_obj = NULL;
+    PyObject *initial_cwd_obj = NULL;
+    PyObject *dummy;
+    bool parse_result;
+
+    lxc_attach_options_t default_options = LXC_ATTACH_OPTIONS_DEFAULT;
+    lxc_attach_options_t *options = malloc(sizeof(*options));
+
+    if (!options) {
+        PyErr_SetNone(PyExc_MemoryError);
+        return NULL;
+    }
+    memcpy(options, &default_options, sizeof(*options));
+
+    /* we need some dummy variables because we can't be sure 
+     * the data types match completely */
+    temp_uid = -1;
+    temp_gid = -1;
+    temp_env_policy = options->env_policy;
+
+    /* we need a dummy tuple */
+    dummy = PyTuple_New(0);
+
+    parse_result = PyArg_ParseTupleAndKeywords(dummy, kwds, "|iilO&lliOOOOO", kwlist,
+                                               &options->attach_flags, &options->namespaces, &options->personality,
+                                               PyUnicode_FSConverter, &initial_cwd_obj, &temp_uid, &temp_gid,
+                                               &temp_env_policy, &extra_env_vars_obj, &extra_keep_env_obj,
+                                               &stdin_obj, &stdout_obj, &stderr_obj);
+
+    /* immediately get rid of the dummy tuple */
+    Py_DECREF(dummy);
+
+    if (!parse_result)
+        return NULL;
+
+    /* duplicate the string, so we don't depend on some random Python object */
+    if (initial_cwd_obj != NULL) {
+        options->initial_cwd = strndup(PyBytes_AsString(initial_cwd_obj), PyBytes_Size(initial_cwd_obj));
+        Py_DECREF(initial_cwd_obj);
+    }
+
+    /* do the type conversion from the types that match the parse string */
+    if (temp_uid != -1) options->uid = (uid_t)temp_uid;
+    if (temp_gid != -1) options->gid = (gid_t)temp_gid;
+    options->env_policy = (lxc_attach_env_policy_t)temp_env_policy;
+
+    if (extra_env_vars_obj)
+        options->extra_env_vars = convert_tuple_to_char_pointer_array(extra_env_vars_obj);
+    if (extra_keep_env_obj)
+        options->extra_keep_env = convert_tuple_to_char_pointer_array(extra_keep_env_obj);
+    if (stdin_obj) {
+        options->stdin_fd = PyObject_AsFileDescriptor(stdin_obj);
+        if (options->stdin_fd < 0)
+            return NULL;
+    }
+    if (stdout_obj) {
+        options->stdout_fd = PyObject_AsFileDescriptor(stdout_obj);
+        if (options->stdout_fd < 0)
+            return NULL;
+    }
+    if (stderr_obj) {
+        options->stderr_fd = PyObject_AsFileDescriptor(stderr_obj);
+        if (options->stderr_fd < 0)
+            return NULL;
+    }
+
+    return options;
+}
+
+void lxc_attach_free_options(lxc_attach_options_t *options)
+{
+    int i;
+    if (!options)
+        return;
+    if (options->initial_cwd)
+        free(options->initial_cwd);
+    if (options->extra_env_vars) {
+        for (i = 0; options->extra_env_vars[i]; i++)
+            free(options->extra_env_vars[i]);
+        free(options->extra_env_vars);
+    }
+    if (options->extra_keep_env) {
+        for (i = 0; options->extra_keep_env[i]; i++)
+            free(options->extra_keep_env[i]);
+        free(options->extra_keep_env);
+    }
+    free(options);
+}
+
+static PyObject *
+Container_attach_and_possibly_wait(Container *self, PyObject *args, PyObject *kwds, int wait)
+{
+    struct lxc_attach_python_payload payload = { NULL, NULL };
+    lxc_attach_options_t *options = NULL;
+    long ret;
+    pid_t pid;
+
+    if (!PyArg_ParseTuple(args, "O|O", &payload.fn, &payload.arg))
+        return NULL;
+    if (!PyCallable_Check(payload.fn)) {
+        PyErr_Format(PyExc_TypeError, "attach: object not callable");
+        return NULL;
+    }
+
+    options = lxc_attach_parse_options(kwds);
+    if (!options)
+        return NULL;
+
+    ret = self->container->attach(self->container, lxc_attach_python_exec, &payload, options, &pid);
+    if (ret < 0)
+        goto out;
+
+    if (wait) {
+        ret = lxc_wait_for_pid_status(pid);
+        /* handle case where attach fails */
+        if (WIFEXITED(ret) && WEXITSTATUS(ret) == 255)
+            ret = -1;
+    } else {
+        ret = (long)pid;
+    }
+
+out:
+    lxc_attach_free_options(options);
+    return PyLong_FromLong(ret);
+}
+
+static PyObject *
+Container_attach(Container *self, PyObject *args, PyObject *kwds)
+{
+    return Container_attach_and_possibly_wait(self, args, kwds, 0);
+}
+
+static PyObject *
+Container_attach_wait(Container *self, PyObject *args, PyObject *kwds)
+{
+    return Container_attach_and_possibly_wait(self, args, kwds, 1);
+}
+
+static PyObject *
+LXC_attach_run_shell(PyObject *self, PyObject *arg)
+{
+    int rv;
+
+    rv = lxc_attach_run_shell(NULL);
+
+    return PyLong_FromLong(rv);
+}
+
+static PyObject *
+LXC_attach_run_command(PyObject *self, PyObject *arg)
+{
+    PyObject *args_obj = NULL;
+    int i, rv;
+    lxc_attach_command_t cmd = {
+        NULL,         /* program */
+        NULL          /* argv[] */
+    };
+
+    if (!PyArg_ParseTuple(arg, "sO", (const char**)&cmd.program, &args_obj))
+        return NULL;
+    if (args_obj && PyList_Check(args_obj)) {
+        cmd.argv = convert_tuple_to_char_pointer_array(args_obj);
+    } else {
+        PyErr_Format(PyExc_TypeError, "Second part of tuple passed to attach_run_command must be a list.");
+        return NULL;
+    }
+
+    if (!cmd.argv)
+        return NULL;
+
+    rv = lxc_attach_run_command(&cmd);
+
+    for (i = 0; cmd.argv[i]; i++)
+        free(cmd.argv[i]);
+    free(cmd.argv);
+
+    return PyLong_FromLong(rv);
+}
+
 static PyGetSetDef Container_getseters[] = {
     {"config_file_name",
      (getter)Container_config_file_name, NULL,
@@ -858,6 +1067,18 @@ static PyMethodDef Container_methods[] = {
      "\n"
      "Attach to container's console."
     },
+    {"attach", (PyCFunction)Container_attach,
+     METH_VARARGS|METH_KEYWORDS,
+     "attach(run, payload) -> int\n"
+     "\n"
+     "Attach to the container. Returns the pid of the attached process."
+    },
+    {"attach_wait", (PyCFunction)Container_attach_wait,
+     METH_VARARGS|METH_KEYWORDS,
+     "attach(run, payload) -> int\n"
+     "\n"
+     "Attach to the container. Returns the exit code of the process."
+    },
     {NULL, NULL, 0, NULL}
 };
 
@@ -904,6 +1125,10 @@ PyVarObject_HEAD_INIT(NULL, 0)
 };
 
 static PyMethodDef LXC_methods[] = {
+    {"attach_run_shell", (PyCFunction)LXC_attach_run_shell, METH_O,
+     "Starts up a shell when attaching, to use as the run parameter for attach or attach_wait"},
+    {"attach_run_command", (PyCFunction)LXC_attach_run_command, METH_O,
+     "Runs a command when attaching, to use as the run parameter for attach or attach_wait"},
     {"get_default_config_path", (PyCFunction)LXC_get_default_config_path, METH_NOARGS,
      "Returns the current LXC config path"},
     {"get_version", (PyCFunction)LXC_get_version, METH_NOARGS,
@@ -923,6 +1148,7 @@ PyMODINIT_FUNC
 PyInit__lxc(void)
 {
     PyObject* m;
+    PyObject* d;
 
     if (PyType_Ready(&_lxc_ContainerType) < 0)
         return NULL;
@@ -933,5 +1159,20 @@ PyInit__lxc(void)
 
     Py_INCREF(&_lxc_ContainerType);
     PyModule_AddObject(m, "Container", (PyObject *)&_lxc_ContainerType);
+
+    /* add constants */
+    d = PyModule_GetDict(m);
+    PyDict_SetItemString(d, "LXC_ATTACH_KEEP_ENV", PyLong_FromLong(LXC_ATTACH_KEEP_ENV));
+    PyDict_SetItemString(d, "LXC_ATTACH_CLEAR_ENV", PyLong_FromLong(LXC_ATTACH_CLEAR_ENV));
+    PyDict_SetItemString(d, "LXC_ATTACH_MOVE_TO_CGROUP", PyLong_FromLong(LXC_ATTACH_MOVE_TO_CGROUP));
+    PyDict_SetItemString(d, "LXC_ATTACH_DROP_CAPABILITIES", PyLong_FromLong(LXC_ATTACH_DROP_CAPABILITIES));
+    PyDict_SetItemString(d, "LXC_ATTACH_SET_PERSONALITY", PyLong_FromLong(LXC_ATTACH_SET_PERSONALITY));
+    PyDict_SetItemString(d, "LXC_ATTACH_APPARMOR", PyLong_FromLong(LXC_ATTACH_APPARMOR));
+    PyDict_SetItemString(d, "LXC_ATTACH_REMOUNT_PROC_SYS", PyLong_FromLong(LXC_ATTACH_REMOUNT_PROC_SYS));
+    PyDict_SetItemString(d, "LXC_ATTACH_DEFAULT", PyLong_FromLong(LXC_ATTACH_DEFAULT));
     return m;
 }
+
+/*
+ * kate: space-indent on; indent-width 4; mixedindent off; indent-mode cstyle;
+ */
diff --git a/src/python-lxc/lxc/__init__.py b/src/python-lxc/lxc/__init__.py
index 4891cd1..1748bac 100644
--- a/src/python-lxc/lxc/__init__.py
+++ b/src/python-lxc/lxc/__init__.py
@@ -230,28 +230,6 @@ class Container(_lxc.Container):
 
         return _lxc.Container.set_config_item(self, key, value)
 
-    def attach(self, namespace="ALL", *cmd):
-        """
-            Attach to a running container.
-        """
-
-        if not self.running:
-            return False
-
-        attach = ["lxc-attach", "-n", self.name,
-                  "-P", self.get_config_path()]
-        if namespace != "ALL":
-            attach += ["-s", namespace]
-
-        if cmd:
-            attach += ["--"] + list(cmd)
-
-        if subprocess.call(
-                attach,
-                universal_newlines=True) != 0:
-            return False
-        return True
-
     def create(self, template, args={}):
         """
             Create a new rootfs for the container.
@@ -446,3 +424,38 @@ def list_containers(as_object=False, config_path=None):
         else:
             containers.append(entry.split("/")[-2])
     return containers
+
+def attach_run_command(cmd):
+    """
+        Run a command when attaching
+        
+        Please do not call directly, this will execvp the command.
+        This is to be used in conjunction with the attach method
+        of a container.
+    """
+    if isinstance(cmd, tuple):
+        return _lxc.attach_run_command(cmd)
+    elif isinstance(cmd, list):
+        return _lxc.attach_run_command((cmd[0], cmd))
+    else:
+        return _lxc.attach_run_command((cmd, [cmd]))
+
+def attach_run_shell():
+    """
+        Run a shell when attaching
+        
+        Please do not call directly, this will execvp the shell.
+        This is to be used in conjunction with the attach method
+        of a container.
+    """
+    return _lxc.attach_run_shell(None)
+
+# Some constants for attach
+LXC_ATTACH_KEEP_ENV = _lxc.LXC_ATTACH_KEEP_ENV
+LXC_ATTACH_CLEAR_ENV = _lxc.LXC_ATTACH_CLEAR_ENV
+LXC_ATTACH_MOVE_TO_CGROUP = _lxc.LXC_ATTACH_MOVE_TO_CGROUP
+LXC_ATTACH_DROP_CAPABILITIES = _lxc.LXC_ATTACH_DROP_CAPABILITIES
+LXC_ATTACH_SET_PERSONALITY = _lxc.LXC_ATTACH_SET_PERSONALITY
+LXC_ATTACH_APPARMOR = _lxc.LXC_ATTACH_APPARMOR
+LXC_ATTACH_REMOUNT_PROC_SYS = _lxc.LXC_ATTACH_REMOUNT_PROC_SYS
+LXC_ATTACH_DEFAULT = _lxc.LXC_ATTACH_DEFAULT
-- 
1.7.10.4





More information about the lxc-devel mailing list