~serge-hallyn/ubuntu/quantal/lxc/lxc-fixapi

« back to all changes in this revision

Viewing changes to debian/local/lxc-ip

  • Committer: Package Import Robot
  • Author(s): Serge Hallyn, Francesco Banconi
  • Date: 2012-05-16 10:46:21 UTC
  • Revision ID: package-import@ubuntu.com-20120516104621-jizhhfxajjyog4ze
Tags: 0.8.0~rc1-4ubuntu7
[ Francesco Banconi ]
* Introduced lxc-ip: retrieve the ip addresses of a container.
* lxc-start-ephemeral: use lxc-ip to ssh to the container (LP: #994752).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# Copyright 2012 Canonical Ltd.  This software is licensed under the
 
3
# GNU Lesser General Public License version 2.1 (see the file COPYING).
 
4
 
 
5
"""Display the ip address of a container."""
 
6
 
 
7
import argparse
 
8
from contextlib import closing, contextmanager
 
9
import ctypes
 
10
import fcntl
 
11
from functools import wraps
 
12
import os
 
13
import socket
 
14
import struct
 
15
import subprocess
 
16
import sys
 
17
 
 
18
 
 
19
# The namespace type to use with setns(2): in this case we want to
 
20
# reassociate this thread with the network namespace.
 
21
CLONE_NEWNET = 0x40000000
 
22
ERRORS = {
 
23
    'not_connected': 'unable to find the container ip address',
 
24
    'not_found': 'the container does not exist or is not running',
 
25
    'not_installed': 'lxc does not seem to be installed',
 
26
    'not_root': 'you must be root',
 
27
    }
 
28
# The ioctl command to retrieve the interface address.
 
29
SIOCGIFADDR = 0x8915
 
30
 
 
31
 
 
32
def _parse_args():
 
33
    """Parse the command line arguments."""
 
34
    parser = argparse.ArgumentParser(description=__doc__)
 
35
    parser.add_argument(
 
36
        '-n', '--name', required=True,
 
37
        help='The name of the container. ')
 
38
    parser.add_argument(
 
39
        '-i', '--interface',
 
40
        help='Display the ip address of the specified network interface.')
 
41
    namespace = parser.parse_args()
 
42
    return namespace.name, namespace.interface
 
43
 
 
44
 
 
45
def _error(code):
 
46
    """Return an OSError containing given `msg`."""
 
47
    return OSError(
 
48
        '{}: error: {}'.format(os.path.basename(sys.argv[0]), ERRORS[code]))
 
49
 
 
50
 
 
51
def _output(interfaces, ip_addresses, short):
 
52
    """Format the output displaying the ip addresses of the container."""
 
53
    if short:
 
54
        return ip_addresses[0]
 
55
    interface_ip_map = zip(interfaces, ip_addresses)
 
56
    return '\n'.join('{}: {}'.format(*i) for i in interface_ip_map)
 
57
 
 
58
 
 
59
def _load_library(name, loader=None):
 
60
    """Load a shared library into the process and return it.
 
61
 
 
62
    Search the library `name` inside `/usr/lib` and, if *$DEB_HOST_MULTIARCH*
 
63
    is retrievable, inside `/usr/lib/$DEB_HOST_MULTIARCH/`.
 
64
 
 
65
    The optional argument `loader` is the callable used to load the library:
 
66
    if None, `ctypes.cdll.LoadLibrary` is used.
 
67
 
 
68
    Raise OSError if the library is not found.
 
69
    """
 
70
    if loader is None:
 
71
        loader = ctypes.cdll.LoadLibrary
 
72
    try:
 
73
        return loader(os.path.join('/usr/lib/', name))
 
74
    except OSError:
 
75
        # Search the library in `/usr/lib/$DEB_HOST_MULTIARCH/`:
 
76
        # see https://wiki.ubuntu.com/MultiarchSpec.
 
77
        process = subprocess.Popen(
 
78
            ['dpkg-architecture', '-qDEB_HOST_MULTIARCH'],
 
79
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
80
        if not process.returncode:
 
81
            output, _ = process.communicate()
 
82
            return loader(os.path.join('/usr/lib/', output.strip(), name))
 
83
        raise
 
84
 
 
85
 
 
86
def _wrap(function, error_code):
 
87
    """Add error handling to the given C `function`.
 
88
 
 
89
    If the function returns an error, the wrapped function raises an
 
90
    OSError using a message corresponding to the given `error_code`.
 
91
    """
 
92
    def errcheck(result, func, arguments):
 
93
        if result < 0:
 
94
            raise _error(error_code)
 
95
        return result
 
96
    function.errcheck = errcheck
 
97
    return function
 
98
 
 
99
 
 
100
@contextmanager
 
101
def redirect_stderr(path):
 
102
    """Redirect system stderr to `path`.
 
103
 
 
104
    This context manager does not use normal sys.std* Python redirection
 
105
    because we also want to intercept and redirect stderr written by
 
106
    underlying C functions called using ctypes.
 
107
    """
 
108
    fd = sys.stderr.fileno()
 
109
    backup = os.dup(fd)
 
110
    new_fd = os.open(path, os.O_WRONLY)
 
111
    sys.stderr.flush()
 
112
    os.dup2(new_fd, fd)
 
113
    os.close(new_fd)
 
114
    try:
 
115
        yield
 
116
    finally:
 
117
        sys.stderr.flush()
 
118
        os.dup2(backup, fd)
 
119
 
 
120
 
 
121
def root_required(func):
 
122
    """A decorator checking for current user effective id.
 
123
 
 
124
    The decorated function is only executed if the current user is root.
 
125
    Otherwise, an OSError is raised.
 
126
    """
 
127
    @wraps(func)
 
128
    def decorated(*args, **kwargs):
 
129
        if os.geteuid():
 
130
            raise _error('not_root')
 
131
        return func(*args, **kwargs)
 
132
    return decorated
 
133
 
 
134
 
 
135
class SetNamespace(object):
 
136
    """A context manager to switch the network namespace for this thread.
 
137
 
 
138
    A namespace is one of the entries in /proc/[pid]/ns/.
 
139
    """
 
140
    def __init__(self, pid, nstype=CLONE_NEWNET):
 
141
        libc = ctypes.cdll.LoadLibrary('libc.so.6')
 
142
        self._pid = pid
 
143
        self._nstype = nstype
 
144
        self._setns = _wrap(libc.setns, 'not_connected')
 
145
 
 
146
    def __enter__(self):
 
147
        """Switch the namespace."""
 
148
        self.set(self._pid)
 
149
        return self
 
150
 
 
151
    def __exit__(self, exc_type, exc_val, exc_tb):
 
152
        """Restore normal namespace."""
 
153
        # To restore the namespace we use the file descriptor associated
 
154
        # with the hosts's init process. In Linux the init pid is always 1.
 
155
        self.set(1)
 
156
 
 
157
    def set(self, pid):
 
158
        try:
 
159
            fd = os.open('/proc/{}/ns/net'.format(pid), os.O_RDONLY)
 
160
        except OSError:
 
161
            raise _error('not_found')
 
162
        self._setns(fd, self._nstype)
 
163
        os.close(fd)
 
164
 
 
165
 
 
166
@root_required
 
167
def get_pid(name):
 
168
    """Return the pid of an LXC, given its `name`.
 
169
 
 
170
    Raise OSError if LXC is not installed or the container is not found.
 
171
    """
 
172
    try:
 
173
        liblxc = _load_library('lxc/liblxc.so.0')
 
174
    except OSError:
 
175
        raise _error('not_installed')
 
176
    get_init_pid = _wrap(liblxc.get_init_pid, 'not_found')
 
177
    # Redirect the system stderr in order to get rid of the error raised by
 
178
    # the underlying C function call if the container is not found.
 
179
    with redirect_stderr('/dev/null'):
 
180
        return get_init_pid(name)
 
181
 
 
182
 
 
183
@root_required
 
184
def get_interfaces(pid, exclude=()):
 
185
    """Return a list of active net interfaces, given the container's `pid`.
 
186
 
 
187
    Raise OSError if the container does not exist, is not running, or if
 
188
    no interface is found.
 
189
    """
 
190
    path = '/proc/{}/root/sys/class/net/'.format(pid)
 
191
    try:
 
192
        interfaces = [
 
193
            i for i in os.listdir(path)
 
194
            if i not in exclude and
 
195
            os.path.isdir(os.path.join(path, i))
 
196
            ]
 
197
    except OSError:
 
198
        raise _error('not_found')
 
199
    if not interfaces:
 
200
        raise _error('not_connected')
 
201
    return interfaces
 
202
 
 
203
 
 
204
@root_required
 
205
def get_ip_addresses(pid, interfaces):
 
206
    """Return ip addresses of LXC `interfaces`, given the container's `pid`.
 
207
 
 
208
    Raise OSError if the container is not found or one the ip addresses
 
209
    is not retrievable.
 
210
 
 
211
    Note that `socket.gethostbyname` is not usable in this context: it uses
 
212
    the system's dns resolver that by default does not resolve lxc names.
 
213
    """
 
214
    ip_addresses = []
 
215
    with SetNamespace(pid):
 
216
        # Retrieve the ip address for the given network interface.
 
217
        # Original from http://code.activestate.com/recipes/
 
218
        #     439094-get-the-ip-address-associated-with-a-network-inter/
 
219
        with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
 
220
            for interface in interfaces:
 
221
                # Slice the interface because the buffer size used to hold an
 
222
                # interface name, including its terminating zero byte,
 
223
                # is 16 in Linux (see /usr/include/linux/if.h).
 
224
                packed = struct.pack('256s', interface[:15])
 
225
                try:
 
226
                    # Use the ioctl Unix routine to request SIOCGIFADDR.
 
227
                    binary_ip = fcntl.ioctl(
 
228
                        s.fileno(), SIOCGIFADDR, packed)[20:24]
 
229
                    # Convert the packet ipv4 address to its standard
 
230
                    # dotted-quad string representation.
 
231
                    ip = socket.inet_ntoa(binary_ip)
 
232
                except (IOError, socket.error):
 
233
                    raise _error('not_connected')
 
234
                ip_addresses.append(ip)
 
235
    return ip_addresses
 
236
 
 
237
 
 
238
def main():
 
239
    name, interface = _parse_args()
 
240
    try:
 
241
        pid = get_pid(name)
 
242
        if interface is None:
 
243
            interfaces = get_interfaces(pid, exclude=['lo'])
 
244
        else:
 
245
            interfaces = [interface]
 
246
        ip_addresses = get_ip_addresses(pid, interfaces)
 
247
    except (KeyboardInterrupt, OSError) as err:
 
248
        return err
 
249
    print _output(interfaces, ip_addresses, interface is not None)
 
250
 
 
251
 
 
252
if __name__ == '__main__':
 
253
    sys.exit(main())