]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/micronet.py
topotests: add bfd_vrflite_topo1 test
[mirror_frr.git] / tests / topotests / lib / micronet.py
CommitLineData
6a5433ef
CH
1# -*- coding: utf-8 eval: (blacken-mode 1) -*-
2#
3# July 9 2021, Christian Hopps <chopps@labn.net>
4#
5# Copyright (c) 2021, LabN Consulting, L.L.C.
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License
9# as published by the Free Software Foundation; either version 2
10# of the License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; see the file COPYING; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20#
21import datetime
22import logging
23import os
6a5433ef 24import re
6a5433ef
CH
25import shlex
26import subprocess
27import sys
28import tempfile
29import time as time_mod
30import traceback
31
32root_hostname = subprocess.check_output("hostname")
33
34# This allows us to cleanup any leftovers later on
35os.environ["MICRONET_PID"] = str(os.getpid())
36
37
38class Timeout(object):
39 def __init__(self, delta):
40 self.started_on = datetime.datetime.now()
41 self.expires_on = self.started_on + datetime.timedelta(seconds=delta)
42
43 def elapsed(self):
44 elapsed = datetime.datetime.now() - self.started_on
45 return elapsed.total_seconds()
46
47 def is_expired(self):
48 return datetime.datetime.now() > self.expires_on
49
50
51def is_string(value):
52 """Return True if value is a string."""
53 try:
54 return isinstance(value, basestring) # type: ignore
55 except NameError:
56 return isinstance(value, str)
57
58
59def shell_quote(command):
60 """Return command wrapped in single quotes."""
61 if sys.version_info[0] >= 3:
62 return shlex.quote(command)
63 return "'{}'".format(command.replace("'", "'\"'\"'")) # type: ignore
64
65
66def cmd_error(rc, o, e):
67 s = "rc {}".format(rc)
68 o = "\n\tstdout: " + o.strip() if o and o.strip() else ""
69 e = "\n\tstderr: " + e.strip() if e and e.strip() else ""
70 return s + o + e
71
72
73def proc_error(p, o, e):
74 args = p.args if is_string(p.args) else " ".join(p.args)
75 s = "rc {} pid {}\n\targs: {}".format(p.returncode, p.pid, args)
76 o = "\n\tstdout: " + o.strip() if o and o.strip() else ""
77 e = "\n\tstderr: " + e.strip() if e and e.strip() else ""
78 return s + o + e
79
80
81def comm_error(p):
82 rc = p.poll()
83 assert rc is not None
84 if not hasattr(p, "saved_output"):
85 p.saved_output = p.communicate()
86 return proc_error(p, *p.saved_output)
87
88
89class Commander(object): # pylint: disable=R0205
90 """
91 Commander.
92
93 An object that can execute commands.
94 """
95
96 tmux_wait_gen = 0
97
98 def __init__(self, name, logger=None):
99 """Create a Commander."""
100 self.name = name
101 self.last = None
102 self.exec_paths = {}
103 self.pre_cmd = []
104 self.pre_cmd_str = ""
105
106 if not logger:
107 self.logger = logging.getLogger(__name__ + ".commander." + name)
108 else:
109 self.logger = logger
110
111 self.cwd = self.cmd_raises("pwd").strip()
112
113 def set_logger(self, logfile):
114 self.logger = logging.getLogger(__name__ + ".commander." + self.name)
115 if is_string(logfile):
116 handler = logging.FileHandler(logfile, mode="w")
117 else:
118 handler = logging.StreamHandler(logfile)
119
120 fmtstr = "%(asctime)s.%(msecs)03d %(levelname)s: {}({}): %(message)s".format(
121 self.__class__.__name__, self.name
122 )
123 handler.setFormatter(logging.Formatter(fmt=fmtstr))
124 self.logger.addHandler(handler)
125
126 def set_pre_cmd(self, pre_cmd=None):
127 if not pre_cmd:
128 self.pre_cmd = []
129 self.pre_cmd_str = ""
130 else:
131 self.pre_cmd = pre_cmd
132 self.pre_cmd_str = " ".join(self.pre_cmd) + " "
133
134 def __str__(self):
135 return "Commander({})".format(self.name)
136
137 def get_exec_path(self, binary):
138 """Return the full path to the binary executable.
139
140 `binary` :: binary name or list of binary names
141 """
142 if is_string(binary):
143 bins = [binary]
144 else:
145 bins = binary
146 for b in bins:
147 if b in self.exec_paths:
148 return self.exec_paths[b]
149
150 rc, output, _ = self.cmd_status("which " + b, warn=False)
151 if not rc:
152 return os.path.abspath(output.strip())
153 return None
154
155 def get_tmp_dir(self, uniq):
156 return os.path.join(tempfile.mkdtemp(), uniq)
157
158 def test(self, flags, arg):
159 """Run test binary, with flags and arg"""
160 test_path = self.get_exec_path(["test"])
161 rc, output, _ = self.cmd_status([test_path, flags, arg], warn=False)
162 return not rc
163
164 def path_exists(self, path):
165 """Check if path exists."""
166 return self.test("-e", path)
167
168 def _get_cmd_str(self, cmd):
169 if is_string(cmd):
170 return self.pre_cmd_str + cmd
171 cmd = self.pre_cmd + cmd
172 return " ".join(cmd)
173
174 def _get_sub_args(self, cmd, defaults, **kwargs):
175 if is_string(cmd):
176 defaults["shell"] = True
177 pre_cmd = self.pre_cmd_str
178 else:
179 defaults["shell"] = False
180 pre_cmd = self.pre_cmd
181 cmd = [str(x) for x in cmd]
182 defaults.update(kwargs)
183 return pre_cmd, cmd, defaults
184
185 def _popen(self, method, cmd, skip_pre_cmd=False, **kwargs):
186 if sys.version_info[0] >= 3:
187 defaults = {
188 "encoding": "utf-8",
189 "stdout": subprocess.PIPE,
190 "stderr": subprocess.PIPE,
191 }
192 else:
193 defaults = {
194 "stdout": subprocess.PIPE,
195 "stderr": subprocess.PIPE,
196 }
197 pre_cmd, cmd, defaults = self._get_sub_args(cmd, defaults, **kwargs)
198
199 self.logger.debug('%s: %s("%s", kwargs: %s)', self, method, cmd, defaults)
200
201 actual_cmd = cmd if skip_pre_cmd else pre_cmd + cmd
202 p = subprocess.Popen(actual_cmd, **defaults)
203 if not hasattr(p, "args"):
204 p.args = actual_cmd
205 return p, actual_cmd
206
207 def set_cwd(self, cwd):
208 self.logger.warning("%s: 'cd' (%s) does not work outside namespaces", self, cwd)
209 self.cwd = cwd
210
211 def popen(self, cmd, **kwargs):
212 """
213 Creates a pipe with the given `command`.
214
215 Args:
216 command: `str` or `list` of command to open a pipe with.
217 **kwargs: kwargs is eventually passed on to Popen. If `command` is a string
218 then will be invoked with shell=True, otherwise `command` is a list and
219 will be invoked with shell=False.
220
221 Returns:
222 a subprocess.Popen object.
223 """
224 p, _ = self._popen("popen", cmd, **kwargs)
225 return p
226
227 def cmd_status(self, cmd, raises=False, warn=True, stdin=None, **kwargs):
228 """Execute a command."""
229
230 # We are not a shell like mininet, so we need to intercept this
231 chdir = False
232 if not is_string(cmd):
233 cmds = cmd
234 else:
235 # XXX we can drop this when the code stops assuming it works
236 m = re.match(r"cd(\s*|\s+(\S+))$", cmd)
237 if m and m.group(2):
238 self.logger.warning(
239 "Bad call to 'cd' (chdir) emulating, use self.set_cwd():\n%s",
240 "".join(traceback.format_stack(limit=12)),
241 )
242 assert is_string(cmd)
243 chdir = True
244 cmd += " && pwd"
245
246 # If we are going to run under bash then we don't need shell=True!
247 cmds = ["/bin/bash", "-c", cmd]
248
249 pinput = None
250
251 if is_string(stdin) or isinstance(stdin, bytes):
252 pinput = stdin
253 stdin = subprocess.PIPE
254
255 p, actual_cmd = self._popen("cmd_status", cmds, stdin=stdin, **kwargs)
256 stdout, stderr = p.communicate(input=pinput)
257 rc = p.wait()
258
259 # For debugging purposes.
260 self.last = (rc, actual_cmd, cmd, stdout, stderr)
261
262 if rc:
263 if warn:
264 self.logger.warning(
265 "%s: proc failed: %s:", self, proc_error(p, stdout, stderr)
266 )
267 if raises:
268 # error = Exception("stderr: {}".format(stderr))
4c98b89e 269 # This annoyingly doesn't' show stderr when printed normally
6a5433ef
CH
270 error = subprocess.CalledProcessError(rc, actual_cmd)
271 error.stdout, error.stderr = stdout, stderr
272 raise error
273 elif chdir:
274 self.set_cwd(stdout.strip())
275
276 return rc, stdout, stderr
277
278 def cmd_legacy(self, cmd, **kwargs):
279 """Execute a command with stdout and stderr joined, *IGNORES ERROR*."""
280
281 defaults = {"stderr": subprocess.STDOUT}
282 defaults.update(kwargs)
283 _, stdout, _ = self.cmd_status(cmd, raises=False, **defaults)
284 return stdout
285
286 def cmd_raises(self, cmd, **kwargs):
287 """Execute a command. Raise an exception on errors"""
288
289 rc, stdout, _ = self.cmd_status(cmd, raises=True, **kwargs)
290 assert rc == 0
291 return stdout
292
293 # Run a command in a new window (gnome-terminal, screen, tmux, xterm)
294 def run_in_window(
295 self,
296 cmd,
297 wait_for=False,
298 background=False,
299 name=None,
300 title=None,
301 forcex=False,
302 new_window=False,
303 tmux_target=None,
304 ):
305 """
306 Run a command in a new window (TMUX, Screen or XTerm).
307
308 Args:
309 wait_for: True to wait for exit from command or `str` as channel neme to signal on exit, otherwise False
310 background: Do not change focus to new window.
311 title: Title for new pane (tmux) or window (xterm).
312 name: Name of the new window (tmux)
313 forcex: Force use of X11.
314 new_window: Open new window (instead of pane) in TMUX
315 tmux_target: Target for tmux pane.
316
317 Returns:
318 the pane/window identifier from TMUX (depends on `new_window`)
319 """
320
321 channel = None
322 if is_string(wait_for):
323 channel = wait_for
324 elif wait_for is True:
325 channel = "{}-wait-{}".format(os.getpid(), Commander.tmux_wait_gen)
326 Commander.tmux_wait_gen += 1
327
328 sudo_path = self.get_exec_path(["sudo"])
329 nscmd = sudo_path + " " + self.pre_cmd_str + cmd
330 if "TMUX" in os.environ and not forcex:
331 cmd = [self.get_exec_path("tmux")]
332 if new_window:
333 cmd.append("new-window")
334 cmd.append("-P")
335 if name:
336 cmd.append("-n")
337 cmd.append(name)
338 if tmux_target:
339 cmd.append("-t")
340 cmd.append(tmux_target)
341 else:
342 cmd.append("split-window")
343 cmd.append("-P")
344 cmd.append("-h")
345 if not tmux_target:
346 tmux_target = os.getenv("TMUX_PANE", "")
347 if background:
348 cmd.append("-d")
349 if tmux_target:
350 cmd.append("-t")
351 cmd.append(tmux_target)
352 if title:
353 nscmd = "printf '\033]2;{}\033\\'; {}".format(title, nscmd)
354 if channel:
355 nscmd = 'trap "tmux wait -S {}; exit 0" EXIT; {}'.format(channel, nscmd)
356 cmd.append(nscmd)
357 elif "STY" in os.environ and not forcex:
358 # wait for not supported in screen for now
359 channel = None
360 cmd = [self.get_exec_path("screen")]
0bc76852
LS
361 if title:
362 cmd.append("-t")
363 cmd.append(title)
6a5433ef
CH
364 if not os.path.exists(
365 "/run/screen/S-{}/{}".format(os.environ["USER"], os.environ["STY"])
366 ):
367 cmd = ["sudo", "-u", os.environ["SUDO_USER"]] + cmd
ce530b41 368 cmd.extend(nscmd.split(" "))
6a5433ef
CH
369 elif "DISPLAY" in os.environ:
370 # We need it broken up for xterm
371 user_cmd = cmd
372 cmd = [self.get_exec_path("xterm")]
373 if "SUDO_USER" in os.environ:
374 cmd = [self.get_exec_path("sudo"), "-u", os.environ["SUDO_USER"]] + cmd
1726edc3
CH
375 if title:
376 cmd.append("-T")
377 cmd.append(title)
6a5433ef
CH
378 cmd.append("-e")
379 cmd.append(sudo_path)
380 cmd.extend(self.pre_cmd)
1726edc3 381 cmd.extend(["bash", "-c", user_cmd])
6a5433ef
CH
382 # if channel:
383 # return self.cmd_raises(cmd, skip_pre_cmd=True)
384 # else:
385 p = self.popen(
386 cmd,
387 skip_pre_cmd=True,
388 stdin=None,
389 shell=False,
6a5433ef
CH
390 )
391 time_mod.sleep(2)
392 if p.poll() is not None:
393 self.logger.error("%s: Failed to launch xterm: %s", self, comm_error(p))
1726edc3 394 return p
6a5433ef
CH
395 else:
396 self.logger.error(
397 "DISPLAY, STY, and TMUX not in environment, can't open window"
398 )
399 raise Exception("Window requestd but TMUX, Screen and X11 not available")
400
401 pane_info = self.cmd_raises(cmd, skip_pre_cmd=True).strip()
402
403 # Re-adjust the layout
404 if "TMUX" in os.environ:
405 self.cmd_status(
406 "tmux select-layout -t {} tiled".format(
407 pane_info if not tmux_target else tmux_target
408 ),
409 skip_pre_cmd=True,
410 )
411
412 # Wait here if we weren't handed the channel to wait for
413 if channel and wait_for is True:
414 cmd = [self.get_exec_path("tmux"), "wait", channel]
415 self.cmd_status(cmd, skip_pre_cmd=True)
416
417 return pane_info
418
419 def delete(self):
420 pass
421
422
423class LinuxNamespace(Commander):
424 """
425 A linux Namespace.
426
427 An object that creates and executes commands in a linux namespace
428 """
429
430 def __init__(
431 self,
432 name,
433 net=True,
434 mount=True,
435 uts=True,
436 cgroup=False,
437 ipc=False,
438 pid=False,
439 time=False,
440 user=False,
441 set_hostname=True,
442 private_mounts=None,
443 logger=None,
444 ):
445 """
446 Create a new linux namespace.
447
448 Args:
449 name: Internal name for the namespace.
450 net: Create network namespace.
451 mount: Create network namespace.
452 uts: Create UTS (hostname) namespace.
453 cgroup: Create cgroup namespace.
454 ipc: Create IPC namespace.
455 pid: Create PID namespace, also mounts new /proc.
456 time: Create time namespace.
457 user: Create user namespace, also keeps capabilities.
458 set_hostname: Set the hostname to `name`, uts must also be True.
459 private_mounts: List of strings of the form
460 "[/external/path:]/internal/path. If no external path is specified a
461 tmpfs is mounted on the internal path. Any paths specified are first
462 passed to `mkdir -p`.
463 logger: Passed to superclass.
464 """
465 super(LinuxNamespace, self).__init__(name, logger)
466
467 self.logger.debug("%s: Creating", self)
468
469 self.intfs = []
470
471 nslist = []
472 cmd = ["/usr/bin/unshare"]
473 flags = "-"
474 self.ifnetns = {}
475
476 if cgroup:
477 nslist.append("cgroup")
478 flags += "C"
479 if ipc:
480 nslist.append("ipc")
481 flags += "i"
482 if mount:
483 nslist.append("mnt")
484 flags += "m"
485 if net:
486 nslist.append("net")
487 flags += "n"
488 if pid:
489 nslist.append("pid")
490 flags += "p"
491 cmd.append("--mount-proc")
492 if time:
493 # XXX this filename is probably wrong
494 nslist.append("time")
495 flags += "T"
496 if user:
497 nslist.append("user")
498 flags += "U"
499 cmd.append("--keep-caps")
500 if uts:
501 nslist.append("uts")
502 cmd.append("--uts")
503
504 cmd.append(flags)
505 cmd.append("/bin/cat")
506
507 # Using cat and a stdin PIPE is nice as it will exit when we do. However, we
508 # also detach it from the pgid so that signals do not propagate to it. This is
509 # b/c it would exit early (e.g., ^C) then, at least the main micronet proc which
510 # has no other processes like frr daemons running, will take the main network
511 # namespace with it, which will remove the bridges and the veth pair (because
512 # the bridge side veth is deleted).
513 self.logger.debug("%s: creating namespace process: %s", self, cmd)
514 p = subprocess.Popen(
515 cmd,
516 stdin=subprocess.PIPE,
517 stdout=open("/dev/null", "w"),
518 stderr=open("/dev/null", "w"),
519 preexec_fn=os.setsid, # detach from pgid so signals don't propogate
520 shell=False,
521 )
522 self.p = p
523 self.pid = p.pid
524
525 self.logger.debug("%s: namespace pid: %d", self, self.pid)
526
527 # -----------------------------------------------
528 # Now let's wait until unshare completes it's job
529 # -----------------------------------------------
530 timeout = Timeout(30)
531 while p.poll() is None and not timeout.is_expired():
532 for fname in tuple(nslist):
533 ours = os.readlink("/proc/self/ns/{}".format(fname))
534 theirs = os.readlink("/proc/{}/ns/{}".format(self.pid, fname))
535 # See if their namespace is different
536 if ours != theirs:
537 nslist.remove(fname)
538 if not nslist:
539 break
540 elapsed = int(timeout.elapsed())
541 if elapsed <= 3:
542 time_mod.sleep(0.1)
543 elif elapsed > 10:
544 self.logger.warning("%s: unshare taking more than %ss", self, elapsed)
545 time_mod.sleep(3)
546 else:
547 self.logger.info("%s: unshare taking more than %ss", self, elapsed)
548 time_mod.sleep(1)
549 assert p.poll() is None, "unshare unexpectedly exited!"
550 assert not nslist, "unshare never unshared!"
551
552 # Set pre-command based on our namespace proc
553 self.base_pre_cmd = ["/usr/bin/nsenter", "-a", "-t", str(self.pid)]
554 if not pid:
555 self.base_pre_cmd.append("-F")
556 self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + self.cwd])
557
49d72f2d 558 # Remount sysfs and cgroup to pickup any changes
6a5433ef 559 self.cmd_raises("mount -t sysfs sysfs /sys")
49d72f2d
LS
560 self.cmd_raises(
561 "mount -o rw,nosuid,nodev,noexec,relatime -t cgroup2 cgroup /sys/fs/cgroup"
562 )
6a5433ef
CH
563
564 # Set the hostname to the namespace name
565 if uts and set_hostname:
566 # Debugging get the root hostname
567 self.cmd_raises("hostname " + self.name)
568 nroot = subprocess.check_output("hostname")
569 if root_hostname != nroot:
570 result = self.p.poll()
571 assert root_hostname == nroot, "STATE of namespace process {}".format(
572 result
573 )
574
575 if private_mounts:
576 if is_string(private_mounts):
577 private_mounts = [private_mounts]
578 for m in private_mounts:
579 s = m.split(":", 1)
580 if len(s) == 1:
581 self.tmpfs_mount(s[0])
582 else:
583 self.bind_mount(s[0], s[1])
584
585 o = self.cmd_legacy("ls -l /proc/{}/ns".format(self.pid))
586 self.logger.debug("namespaces:\n %s", o)
587
588 # Doing this here messes up all_protocols ipv6 check
589 self.cmd_raises("ip link set lo up")
590
591 def __str__(self):
592 return "LinuxNamespace({})".format(self.name)
593
594 def tmpfs_mount(self, inner):
595 self.cmd_raises("mkdir -p " + inner)
596 self.cmd_raises("mount -n -t tmpfs tmpfs " + inner)
597
598 def bind_mount(self, outer, inner):
599 self.cmd_raises("mkdir -p " + inner)
600 self.cmd_raises("mount --rbind {} {} ".format(outer, inner))
601
97413ed7
PG
602 def add_vlan(self, vlanname, linkiface, vlanid):
603 self.logger.debug("Adding VLAN interface: %s (%s)", vlanname, vlanid)
604 ip_path = self.get_exec_path("ip")
605 assert ip_path, "XXX missing ip command!"
606 self.cmd_raises(
607 [
608 ip_path,
609 "link",
610 "add",
611 "link",
612 linkiface,
613 "name",
614 vlanname,
615 "type",
616 "vlan",
617 "id",
618 vlanid,
619 ]
620 )
621 self.cmd_raises([ip_path, "link", "set", "dev", vlanname, "up"])
622
623 def add_loop(self, loopname):
624 self.logger.debug("Adding Linux iface: %s", loopname)
625 ip_path = self.get_exec_path("ip")
626 assert ip_path, "XXX missing ip command!"
627 self.cmd_raises([ip_path, "link", "add", loopname, "type", "dummy"])
628 self.cmd_raises([ip_path, "link", "set", "dev", loopname, "up"])
629
630 def add_l3vrf(self, vrfname, tableid):
631 self.logger.debug("Adding Linux VRF: %s", vrfname)
632 ip_path = self.get_exec_path("ip")
633 assert ip_path, "XXX missing ip command!"
634 self.cmd_raises(
635 [ip_path, "link", "add", vrfname, "type", "vrf", "table", tableid]
636 )
637 self.cmd_raises([ip_path, "link", "set", "dev", vrfname, "up"])
638
639 def del_iface(self, iface):
640 self.logger.debug("Removing Linux Iface: %s", iface)
641 ip_path = self.get_exec_path("ip")
642 assert ip_path, "XXX missing ip command!"
643 self.cmd_raises([ip_path, "link", "del", iface])
644
645 def attach_iface_to_l3vrf(self, ifacename, vrfname):
646 self.logger.debug("Attaching Iface %s to Linux VRF %s", ifacename, vrfname)
647 ip_path = self.get_exec_path("ip")
648 assert ip_path, "XXX missing ip command!"
649 if vrfname:
650 self.cmd_raises(
651 [ip_path, "link", "set", "dev", ifacename, "master", vrfname]
652 )
653 else:
654 self.cmd_raises([ip_path, "link", "set", "dev", ifacename, "nomaster"])
655
6a5433ef
CH
656 def add_netns(self, ns):
657 self.logger.debug("Adding network namespace %s", ns)
658
659 ip_path = self.get_exec_path("ip")
660 assert ip_path, "XXX missing ip command!"
661 if os.path.exists("/run/netns/{}".format(ns)):
662 self.logger.warning("%s: Removing existing nsspace %s", self, ns)
663 try:
664 self.delete_netns(ns)
665 except Exception as ex:
666 self.logger.warning(
667 "%s: Couldn't remove existing nsspace %s: %s",
668 self,
669 ns,
670 str(ex),
671 exc_info=True,
672 )
673 self.cmd_raises([ip_path, "netns", "add", ns])
674
675 def delete_netns(self, ns):
676 self.logger.debug("Deleting network namespace %s", ns)
677
678 ip_path = self.get_exec_path("ip")
679 assert ip_path, "XXX missing ip command!"
680 self.cmd_raises([ip_path, "netns", "delete", ns])
681
682 def set_intf_netns(self, intf, ns, up=False):
683 # In case a user hard-codes 1 thinking it "resets"
684 ns = str(ns)
685 if ns == "1":
686 ns = str(self.pid)
687
688 self.logger.debug("Moving interface %s to namespace %s", intf, ns)
689
690 cmd = "ip link set {} netns " + ns
691 if up:
692 cmd += " up"
693 self.intf_ip_cmd(intf, cmd)
694 if ns == str(self.pid):
695 # If we are returning then remove from dict
696 if intf in self.ifnetns:
697 del self.ifnetns[intf]
698 else:
699 self.ifnetns[intf] = ns
700
701 def reset_intf_netns(self, intf):
702 self.logger.debug("Moving interface %s to default namespace", intf)
703 self.set_intf_netns(intf, str(self.pid))
704
705 def intf_ip_cmd(self, intf, cmd):
706 """Run an ip command for considering an interfaces possible namespace.
707
708 `cmd` - format is run using the interface name on the command
709 """
710 if intf in self.ifnetns:
711 assert cmd.startswith("ip ")
712 cmd = "ip -n " + self.ifnetns[intf] + cmd[2:]
713 self.cmd_raises(cmd.format(intf))
714
715 def set_cwd(self, cwd):
716 # Set pre-command based on our namespace proc
717 self.logger.debug("%s: new CWD %s", self, cwd)
718 self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + cwd])
719
720 def register_interface(self, ifname):
721 if ifname not in self.intfs:
722 self.intfs.append(ifname)
723
724 def delete(self):
725 if self.p and self.p.poll() is None:
726 if sys.version_info[0] >= 3:
727 try:
728 self.p.terminate()
729 self.p.communicate(timeout=10)
730 except subprocess.TimeoutExpired:
731 self.p.kill()
732 self.p.communicate(timeout=2)
733 else:
734 self.p.kill()
735 self.p.communicate()
736 self.set_pre_cmd(["/bin/false"])
737
738
739class SharedNamespace(Commander):
740 """
741 Share another namespace.
742
743 An object that executes commands in an existing pid's linux namespace
744 """
745
746 def __init__(self, name, pid, logger=None):
747 """
748 Share a linux namespace.
749
750 Args:
751 name: Internal name for the namespace.
752 pid: PID of the process to share with.
753 """
754 super(SharedNamespace, self).__init__(name, logger)
755
756 self.logger.debug("%s: Creating", self)
757
758 self.pid = pid
759 self.intfs = []
760
761 # Set pre-command based on our namespace proc
762 self.set_pre_cmd(
763 ["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + self.cwd]
764 )
765
766 def __str__(self):
767 return "SharedNamespace({})".format(self.name)
768
769 def set_cwd(self, cwd):
770 # Set pre-command based on our namespace proc
771 self.logger.debug("%s: new CWD %s", self, cwd)
772 self.set_pre_cmd(["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + cwd])
773
774 def register_interface(self, ifname):
775 if ifname not in self.intfs:
776 self.intfs.append(ifname)
777
778
779class Bridge(SharedNamespace):
780 """
781 A linux bridge.
782 """
783
784 next_brid_ord = 0
785
786 @classmethod
787 def _get_next_brid(cls):
788 brid_ord = cls.next_brid_ord
789 cls.next_brid_ord += 1
790 return brid_ord
791
792 def __init__(self, name=None, unet=None, logger=None):
793 """Create a linux Bridge."""
794
795 self.unet = unet
796 self.brid_ord = self._get_next_brid()
797 if name:
798 self.brid = name
799 else:
800 self.brid = "br{}".format(self.brid_ord)
801 name = self.brid
802
803 super(Bridge, self).__init__(name, unet.pid, logger)
804
805 self.logger.debug("Bridge: Creating")
806
807 assert len(self.brid) <= 16 # Make sure fits in IFNAMSIZE
808 self.cmd_raises("ip link delete {} || true".format(self.brid))
809 self.cmd_raises("ip link add {} type bridge".format(self.brid))
810 self.cmd_raises("ip link set {} up".format(self.brid))
811
812 self.logger.debug("%s: Created, Running", self)
813
814 def __str__(self):
815 return "Bridge({})".format(self.brid)
816
817 def delete(self):
818 """Stop the bridge (i.e., delete the linux resources)."""
819
820 rc, o, e = self.cmd_status("ip link show {}".format(self.brid), warn=False)
821 if not rc:
822 rc, o, e = self.cmd_status(
823 "ip link delete {}".format(self.brid), warn=False
824 )
825 if rc:
826 self.logger.error(
827 "%s: error deleting bridge %s: %s",
828 self,
829 self.brid,
830 cmd_error(rc, o, e),
831 )
832 else:
833 self.logger.debug("%s: Deleted.", self)
834
835
836class Micronet(LinuxNamespace): # pylint: disable=R0205
837 """
838 Micronet.
839 """
840
841 def __init__(self):
842 """Create a Micronet."""
843
844 self.hosts = {}
845 self.switches = {}
846 self.links = {}
847 self.macs = {}
848 self.rmacs = {}
849
850 super(Micronet, self).__init__("micronet", mount=True, net=True, uts=True)
851
852 self.logger.debug("%s: Creating", self)
853
854 def __str__(self):
855 return "Micronet()"
856
857 def __getitem__(self, key):
858 if key in self.switches:
859 return self.switches[key]
860 return self.hosts[key]
861
862 def add_host(self, name, cls=LinuxNamespace, **kwargs):
863 """Add a host to micronet."""
864
865 self.logger.debug("%s: add_host %s", self, name)
866
867 self.hosts[name] = cls(name, **kwargs)
868 # Create a new mounted FS for tracking nested network namespaces creatd by the
869 # user with `ip netns add`
870 self.hosts[name].tmpfs_mount("/run/netns")
871
872 def add_link(self, name1, name2, if1, if2):
873 """Add a link between switch and host to micronet."""
874 isp2p = False
875 if name1 in self.switches:
876 assert name2 in self.hosts
877 elif name2 in self.switches:
878 assert name1 in self.hosts
879 name1, name2 = name2, name1
880 if1, if2 = if2, if1
881 else:
882 # p2p link
883 assert name1 in self.hosts
884 assert name2 in self.hosts
885 isp2p = True
886
887 lname = "{}:{}-{}:{}".format(name1, if1, name2, if2)
888 self.logger.debug("%s: add_link %s%s", self, lname, " p2p" if isp2p else "")
889 self.links[lname] = (name1, if1, name2, if2)
890
891 # And create the veth now.
892 if isp2p:
893 lhost, rhost = self.hosts[name1], self.hosts[name2]
894 lifname = "i1{:x}".format(lhost.pid)
895 rifname = "i2{:x}".format(rhost.pid)
896 self.cmd_raises(
897 "ip link add {} type veth peer name {}".format(lifname, rifname)
898 )
899
900 self.cmd_raises("ip link set {} netns {}".format(lifname, lhost.pid))
901 lhost.cmd_raises("ip link set {} name {}".format(lifname, if1))
902 lhost.cmd_raises("ip link set {} up".format(if1))
903 lhost.register_interface(if1)
904
905 self.cmd_raises("ip link set {} netns {}".format(rifname, rhost.pid))
906 rhost.cmd_raises("ip link set {} name {}".format(rifname, if2))
907 rhost.cmd_raises("ip link set {} up".format(if2))
908 rhost.register_interface(if2)
909 else:
910 switch = self.switches[name1]
911 host = self.hosts[name2]
912
913 assert len(if1) <= 16 and len(if2) <= 16 # Make sure fits in IFNAMSIZE
914
915 self.logger.debug("%s: Creating veth pair for link %s", self, lname)
916 self.cmd_raises(
917 "ip link add {} type veth peer name {} netns {}".format(
918 if1, if2, host.pid
919 )
920 )
921 self.cmd_raises("ip link set {} netns {}".format(if1, switch.pid))
922 switch.register_interface(if1)
923 host.register_interface(if2)
924 self.cmd_raises("ip link set {} master {}".format(if1, switch.brid))
925 self.cmd_raises("ip link set {} up".format(if1))
926 host.cmd_raises("ip link set {} up".format(if2))
927
928 # Cache the MAC values, and reverse mapping
929 self.get_mac(name1, if1)
930 self.get_mac(name2, if2)
931
932 def add_switch(self, name):
933 """Add a switch to micronet."""
934
935 self.logger.debug("%s: add_switch %s", self, name)
936 self.switches[name] = Bridge(name, self)
937
938 def get_mac(self, name, ifname):
939 if name in self.hosts:
940 dev = self.hosts[name]
941 else:
942 dev = self.switches[name]
943
944 if (name, ifname) not in self.macs:
945 _, output, _ = dev.cmd_status("ip -o link show " + ifname)
946 m = re.match(".*link/(loopback|ether) ([0-9a-fA-F:]+) .*", output)
947 mac = m.group(2)
948 self.macs[(name, ifname)] = mac
949 self.rmacs[mac] = (name, ifname)
950
951 return self.macs[(name, ifname)]
952
953 def delete(self):
954 """Delete the micronet topology."""
955
956 self.logger.debug("%s: Deleting.", self)
957
958 for lname, (_, _, rname, rif) in self.links.items():
959 host = self.hosts[rname]
960
961 self.logger.debug("%s: Deleting veth pair for link %s", self, lname)
962
963 rc, o, e = host.cmd_status("ip link delete {}".format(rif), warn=False)
964 if rc:
965 self.logger.error(
966 "Error deleting veth pair %s: %s", lname, cmd_error(rc, o, e)
967 )
968
969 self.links = {}
970
971 for host in self.hosts.values():
972 try:
973 host.delete()
974 except Exception as error:
975 self.logger.error(
976 "%s: error while deleting host %s: %s", self, host, error
977 )
978
979 self.hosts = {}
980
981 for switch in self.switches.values():
982 try:
983 switch.delete()
984 except Exception as error:
985 self.logger.error(
986 "%s: error while deleting switch %s: %s", self, switch, error
987 )
988 self.switches = {}
989
990 self.logger.debug("%s: Deleted.", self)
991
992 super(Micronet, self).delete()
993
994
995# ---------------------------
996# Root level utility function
997# ---------------------------
998
999
1000def get_exec_path(binary):
1001 base = Commander("base")
1002 return base.get_exec_path(binary)
1003
1004
1005commander = Commander("micronet")