]> git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/micronet.py
*: Add camelCase JSON keys in addition to PascalCase
[mirror_frr.git] / tests / topotests / lib / micronet.py
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 #
21 import datetime
22 import logging
23 import os
24 import re
25 import shlex
26 import subprocess
27 import sys
28 import tempfile
29 import time as time_mod
30 import traceback
31
32 root_hostname = subprocess.check_output("hostname")
33
34 # This allows us to cleanup any leftovers later on
35 os.environ["MICRONET_PID"] = str(os.getpid())
36
37
38 class 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
51 def 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
59 def 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
66 def 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
73 def 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
81 def 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
89 class 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))
269 # This annoyingly doesnt' show stderr when printed normally
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")]
361 if not os.path.exists(
362 "/run/screen/S-{}/{}".format(os.environ["USER"], os.environ["STY"])
363 ):
364 cmd = ["sudo", "-u", os.environ["SUDO_USER"]] + cmd
365 cmd.append(nscmd)
366 elif "DISPLAY" in os.environ:
367 # We need it broken up for xterm
368 user_cmd = cmd
369 cmd = [self.get_exec_path("xterm")]
370 if "SUDO_USER" in os.environ:
371 cmd = [self.get_exec_path("sudo"), "-u", os.environ["SUDO_USER"]] + cmd
372 if title:
373 cmd.append("-T")
374 cmd.append(title)
375 cmd.append("-e")
376 cmd.append(sudo_path)
377 cmd.extend(self.pre_cmd)
378 cmd.extend(["bash", "-c", user_cmd])
379 # if channel:
380 # return self.cmd_raises(cmd, skip_pre_cmd=True)
381 # else:
382 p = self.popen(
383 cmd,
384 skip_pre_cmd=True,
385 stdin=None,
386 shell=False,
387 )
388 time_mod.sleep(2)
389 if p.poll() is not None:
390 self.logger.error("%s: Failed to launch xterm: %s", self, comm_error(p))
391 return p
392 else:
393 self.logger.error(
394 "DISPLAY, STY, and TMUX not in environment, can't open window"
395 )
396 raise Exception("Window requestd but TMUX, Screen and X11 not available")
397
398 pane_info = self.cmd_raises(cmd, skip_pre_cmd=True).strip()
399
400 # Re-adjust the layout
401 if "TMUX" in os.environ:
402 self.cmd_status(
403 "tmux select-layout -t {} tiled".format(
404 pane_info if not tmux_target else tmux_target
405 ),
406 skip_pre_cmd=True,
407 )
408
409 # Wait here if we weren't handed the channel to wait for
410 if channel and wait_for is True:
411 cmd = [self.get_exec_path("tmux"), "wait", channel]
412 self.cmd_status(cmd, skip_pre_cmd=True)
413
414 return pane_info
415
416 def delete(self):
417 pass
418
419
420 class LinuxNamespace(Commander):
421 """
422 A linux Namespace.
423
424 An object that creates and executes commands in a linux namespace
425 """
426
427 def __init__(
428 self,
429 name,
430 net=True,
431 mount=True,
432 uts=True,
433 cgroup=False,
434 ipc=False,
435 pid=False,
436 time=False,
437 user=False,
438 set_hostname=True,
439 private_mounts=None,
440 logger=None,
441 ):
442 """
443 Create a new linux namespace.
444
445 Args:
446 name: Internal name for the namespace.
447 net: Create network namespace.
448 mount: Create network namespace.
449 uts: Create UTS (hostname) namespace.
450 cgroup: Create cgroup namespace.
451 ipc: Create IPC namespace.
452 pid: Create PID namespace, also mounts new /proc.
453 time: Create time namespace.
454 user: Create user namespace, also keeps capabilities.
455 set_hostname: Set the hostname to `name`, uts must also be True.
456 private_mounts: List of strings of the form
457 "[/external/path:]/internal/path. If no external path is specified a
458 tmpfs is mounted on the internal path. Any paths specified are first
459 passed to `mkdir -p`.
460 logger: Passed to superclass.
461 """
462 super(LinuxNamespace, self).__init__(name, logger)
463
464 self.logger.debug("%s: Creating", self)
465
466 self.intfs = []
467
468 nslist = []
469 cmd = ["/usr/bin/unshare"]
470 flags = "-"
471 self.ifnetns = {}
472
473 if cgroup:
474 nslist.append("cgroup")
475 flags += "C"
476 if ipc:
477 nslist.append("ipc")
478 flags += "i"
479 if mount:
480 nslist.append("mnt")
481 flags += "m"
482 if net:
483 nslist.append("net")
484 flags += "n"
485 if pid:
486 nslist.append("pid")
487 flags += "p"
488 cmd.append("--mount-proc")
489 if time:
490 # XXX this filename is probably wrong
491 nslist.append("time")
492 flags += "T"
493 if user:
494 nslist.append("user")
495 flags += "U"
496 cmd.append("--keep-caps")
497 if uts:
498 nslist.append("uts")
499 cmd.append("--uts")
500
501 cmd.append(flags)
502 cmd.append("/bin/cat")
503
504 # Using cat and a stdin PIPE is nice as it will exit when we do. However, we
505 # also detach it from the pgid so that signals do not propagate to it. This is
506 # b/c it would exit early (e.g., ^C) then, at least the main micronet proc which
507 # has no other processes like frr daemons running, will take the main network
508 # namespace with it, which will remove the bridges and the veth pair (because
509 # the bridge side veth is deleted).
510 self.logger.debug("%s: creating namespace process: %s", self, cmd)
511 p = subprocess.Popen(
512 cmd,
513 stdin=subprocess.PIPE,
514 stdout=open("/dev/null", "w"),
515 stderr=open("/dev/null", "w"),
516 preexec_fn=os.setsid, # detach from pgid so signals don't propogate
517 shell=False,
518 )
519 self.p = p
520 self.pid = p.pid
521
522 self.logger.debug("%s: namespace pid: %d", self, self.pid)
523
524 # -----------------------------------------------
525 # Now let's wait until unshare completes it's job
526 # -----------------------------------------------
527 timeout = Timeout(30)
528 while p.poll() is None and not timeout.is_expired():
529 for fname in tuple(nslist):
530 ours = os.readlink("/proc/self/ns/{}".format(fname))
531 theirs = os.readlink("/proc/{}/ns/{}".format(self.pid, fname))
532 # See if their namespace is different
533 if ours != theirs:
534 nslist.remove(fname)
535 if not nslist:
536 break
537 elapsed = int(timeout.elapsed())
538 if elapsed <= 3:
539 time_mod.sleep(0.1)
540 elif elapsed > 10:
541 self.logger.warning("%s: unshare taking more than %ss", self, elapsed)
542 time_mod.sleep(3)
543 else:
544 self.logger.info("%s: unshare taking more than %ss", self, elapsed)
545 time_mod.sleep(1)
546 assert p.poll() is None, "unshare unexpectedly exited!"
547 assert not nslist, "unshare never unshared!"
548
549 # Set pre-command based on our namespace proc
550 self.base_pre_cmd = ["/usr/bin/nsenter", "-a", "-t", str(self.pid)]
551 if not pid:
552 self.base_pre_cmd.append("-F")
553 self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + self.cwd])
554
555 # Remount /sys to pickup any changes
556 self.cmd_raises("mount -t sysfs sysfs /sys")
557
558 # Set the hostname to the namespace name
559 if uts and set_hostname:
560 # Debugging get the root hostname
561 self.cmd_raises("hostname " + self.name)
562 nroot = subprocess.check_output("hostname")
563 if root_hostname != nroot:
564 result = self.p.poll()
565 assert root_hostname == nroot, "STATE of namespace process {}".format(
566 result
567 )
568
569 if private_mounts:
570 if is_string(private_mounts):
571 private_mounts = [private_mounts]
572 for m in private_mounts:
573 s = m.split(":", 1)
574 if len(s) == 1:
575 self.tmpfs_mount(s[0])
576 else:
577 self.bind_mount(s[0], s[1])
578
579 o = self.cmd_legacy("ls -l /proc/{}/ns".format(self.pid))
580 self.logger.debug("namespaces:\n %s", o)
581
582 # Doing this here messes up all_protocols ipv6 check
583 self.cmd_raises("ip link set lo up")
584
585 def __str__(self):
586 return "LinuxNamespace({})".format(self.name)
587
588 def tmpfs_mount(self, inner):
589 self.cmd_raises("mkdir -p " + inner)
590 self.cmd_raises("mount -n -t tmpfs tmpfs " + inner)
591
592 def bind_mount(self, outer, inner):
593 self.cmd_raises("mkdir -p " + inner)
594 self.cmd_raises("mount --rbind {} {} ".format(outer, inner))
595
596 def add_netns(self, ns):
597 self.logger.debug("Adding network namespace %s", ns)
598
599 ip_path = self.get_exec_path("ip")
600 assert ip_path, "XXX missing ip command!"
601 if os.path.exists("/run/netns/{}".format(ns)):
602 self.logger.warning("%s: Removing existing nsspace %s", self, ns)
603 try:
604 self.delete_netns(ns)
605 except Exception as ex:
606 self.logger.warning(
607 "%s: Couldn't remove existing nsspace %s: %s",
608 self,
609 ns,
610 str(ex),
611 exc_info=True,
612 )
613 self.cmd_raises([ip_path, "netns", "add", ns])
614
615 def delete_netns(self, ns):
616 self.logger.debug("Deleting network namespace %s", ns)
617
618 ip_path = self.get_exec_path("ip")
619 assert ip_path, "XXX missing ip command!"
620 self.cmd_raises([ip_path, "netns", "delete", ns])
621
622 def set_intf_netns(self, intf, ns, up=False):
623 # In case a user hard-codes 1 thinking it "resets"
624 ns = str(ns)
625 if ns == "1":
626 ns = str(self.pid)
627
628 self.logger.debug("Moving interface %s to namespace %s", intf, ns)
629
630 cmd = "ip link set {} netns " + ns
631 if up:
632 cmd += " up"
633 self.intf_ip_cmd(intf, cmd)
634 if ns == str(self.pid):
635 # If we are returning then remove from dict
636 if intf in self.ifnetns:
637 del self.ifnetns[intf]
638 else:
639 self.ifnetns[intf] = ns
640
641 def reset_intf_netns(self, intf):
642 self.logger.debug("Moving interface %s to default namespace", intf)
643 self.set_intf_netns(intf, str(self.pid))
644
645 def intf_ip_cmd(self, intf, cmd):
646 """Run an ip command for considering an interfaces possible namespace.
647
648 `cmd` - format is run using the interface name on the command
649 """
650 if intf in self.ifnetns:
651 assert cmd.startswith("ip ")
652 cmd = "ip -n " + self.ifnetns[intf] + cmd[2:]
653 self.cmd_raises(cmd.format(intf))
654
655 def set_cwd(self, cwd):
656 # Set pre-command based on our namespace proc
657 self.logger.debug("%s: new CWD %s", self, cwd)
658 self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + cwd])
659
660 def register_interface(self, ifname):
661 if ifname not in self.intfs:
662 self.intfs.append(ifname)
663
664 def delete(self):
665 if self.p and self.p.poll() is None:
666 if sys.version_info[0] >= 3:
667 try:
668 self.p.terminate()
669 self.p.communicate(timeout=10)
670 except subprocess.TimeoutExpired:
671 self.p.kill()
672 self.p.communicate(timeout=2)
673 else:
674 self.p.kill()
675 self.p.communicate()
676 self.set_pre_cmd(["/bin/false"])
677
678
679 class SharedNamespace(Commander):
680 """
681 Share another namespace.
682
683 An object that executes commands in an existing pid's linux namespace
684 """
685
686 def __init__(self, name, pid, logger=None):
687 """
688 Share a linux namespace.
689
690 Args:
691 name: Internal name for the namespace.
692 pid: PID of the process to share with.
693 """
694 super(SharedNamespace, self).__init__(name, logger)
695
696 self.logger.debug("%s: Creating", self)
697
698 self.pid = pid
699 self.intfs = []
700
701 # Set pre-command based on our namespace proc
702 self.set_pre_cmd(
703 ["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + self.cwd]
704 )
705
706 def __str__(self):
707 return "SharedNamespace({})".format(self.name)
708
709 def set_cwd(self, cwd):
710 # Set pre-command based on our namespace proc
711 self.logger.debug("%s: new CWD %s", self, cwd)
712 self.set_pre_cmd(["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + cwd])
713
714 def register_interface(self, ifname):
715 if ifname not in self.intfs:
716 self.intfs.append(ifname)
717
718
719 class Bridge(SharedNamespace):
720 """
721 A linux bridge.
722 """
723
724 next_brid_ord = 0
725
726 @classmethod
727 def _get_next_brid(cls):
728 brid_ord = cls.next_brid_ord
729 cls.next_brid_ord += 1
730 return brid_ord
731
732 def __init__(self, name=None, unet=None, logger=None):
733 """Create a linux Bridge."""
734
735 self.unet = unet
736 self.brid_ord = self._get_next_brid()
737 if name:
738 self.brid = name
739 else:
740 self.brid = "br{}".format(self.brid_ord)
741 name = self.brid
742
743 super(Bridge, self).__init__(name, unet.pid, logger)
744
745 self.logger.debug("Bridge: Creating")
746
747 assert len(self.brid) <= 16 # Make sure fits in IFNAMSIZE
748 self.cmd_raises("ip link delete {} || true".format(self.brid))
749 self.cmd_raises("ip link add {} type bridge".format(self.brid))
750 self.cmd_raises("ip link set {} up".format(self.brid))
751
752 self.logger.debug("%s: Created, Running", self)
753
754 def __str__(self):
755 return "Bridge({})".format(self.brid)
756
757 def delete(self):
758 """Stop the bridge (i.e., delete the linux resources)."""
759
760 rc, o, e = self.cmd_status("ip link show {}".format(self.brid), warn=False)
761 if not rc:
762 rc, o, e = self.cmd_status(
763 "ip link delete {}".format(self.brid), warn=False
764 )
765 if rc:
766 self.logger.error(
767 "%s: error deleting bridge %s: %s",
768 self,
769 self.brid,
770 cmd_error(rc, o, e),
771 )
772 else:
773 self.logger.debug("%s: Deleted.", self)
774
775
776 class Micronet(LinuxNamespace): # pylint: disable=R0205
777 """
778 Micronet.
779 """
780
781 def __init__(self):
782 """Create a Micronet."""
783
784 self.hosts = {}
785 self.switches = {}
786 self.links = {}
787 self.macs = {}
788 self.rmacs = {}
789
790 super(Micronet, self).__init__("micronet", mount=True, net=True, uts=True)
791
792 self.logger.debug("%s: Creating", self)
793
794 def __str__(self):
795 return "Micronet()"
796
797 def __getitem__(self, key):
798 if key in self.switches:
799 return self.switches[key]
800 return self.hosts[key]
801
802 def add_host(self, name, cls=LinuxNamespace, **kwargs):
803 """Add a host to micronet."""
804
805 self.logger.debug("%s: add_host %s", self, name)
806
807 self.hosts[name] = cls(name, **kwargs)
808 # Create a new mounted FS for tracking nested network namespaces creatd by the
809 # user with `ip netns add`
810 self.hosts[name].tmpfs_mount("/run/netns")
811
812 def add_link(self, name1, name2, if1, if2):
813 """Add a link between switch and host to micronet."""
814 isp2p = False
815 if name1 in self.switches:
816 assert name2 in self.hosts
817 elif name2 in self.switches:
818 assert name1 in self.hosts
819 name1, name2 = name2, name1
820 if1, if2 = if2, if1
821 else:
822 # p2p link
823 assert name1 in self.hosts
824 assert name2 in self.hosts
825 isp2p = True
826
827 lname = "{}:{}-{}:{}".format(name1, if1, name2, if2)
828 self.logger.debug("%s: add_link %s%s", self, lname, " p2p" if isp2p else "")
829 self.links[lname] = (name1, if1, name2, if2)
830
831 # And create the veth now.
832 if isp2p:
833 lhost, rhost = self.hosts[name1], self.hosts[name2]
834 lifname = "i1{:x}".format(lhost.pid)
835 rifname = "i2{:x}".format(rhost.pid)
836 self.cmd_raises(
837 "ip link add {} type veth peer name {}".format(lifname, rifname)
838 )
839
840 self.cmd_raises("ip link set {} netns {}".format(lifname, lhost.pid))
841 lhost.cmd_raises("ip link set {} name {}".format(lifname, if1))
842 lhost.cmd_raises("ip link set {} up".format(if1))
843 lhost.register_interface(if1)
844
845 self.cmd_raises("ip link set {} netns {}".format(rifname, rhost.pid))
846 rhost.cmd_raises("ip link set {} name {}".format(rifname, if2))
847 rhost.cmd_raises("ip link set {} up".format(if2))
848 rhost.register_interface(if2)
849 else:
850 switch = self.switches[name1]
851 host = self.hosts[name2]
852
853 assert len(if1) <= 16 and len(if2) <= 16 # Make sure fits in IFNAMSIZE
854
855 self.logger.debug("%s: Creating veth pair for link %s", self, lname)
856 self.cmd_raises(
857 "ip link add {} type veth peer name {} netns {}".format(
858 if1, if2, host.pid
859 )
860 )
861 self.cmd_raises("ip link set {} netns {}".format(if1, switch.pid))
862 switch.register_interface(if1)
863 host.register_interface(if2)
864 self.cmd_raises("ip link set {} master {}".format(if1, switch.brid))
865 self.cmd_raises("ip link set {} up".format(if1))
866 host.cmd_raises("ip link set {} up".format(if2))
867
868 # Cache the MAC values, and reverse mapping
869 self.get_mac(name1, if1)
870 self.get_mac(name2, if2)
871
872 def add_switch(self, name):
873 """Add a switch to micronet."""
874
875 self.logger.debug("%s: add_switch %s", self, name)
876 self.switches[name] = Bridge(name, self)
877
878 def get_mac(self, name, ifname):
879 if name in self.hosts:
880 dev = self.hosts[name]
881 else:
882 dev = self.switches[name]
883
884 if (name, ifname) not in self.macs:
885 _, output, _ = dev.cmd_status("ip -o link show " + ifname)
886 m = re.match(".*link/(loopback|ether) ([0-9a-fA-F:]+) .*", output)
887 mac = m.group(2)
888 self.macs[(name, ifname)] = mac
889 self.rmacs[mac] = (name, ifname)
890
891 return self.macs[(name, ifname)]
892
893 def delete(self):
894 """Delete the micronet topology."""
895
896 self.logger.debug("%s: Deleting.", self)
897
898 for lname, (_, _, rname, rif) in self.links.items():
899 host = self.hosts[rname]
900
901 self.logger.debug("%s: Deleting veth pair for link %s", self, lname)
902
903 rc, o, e = host.cmd_status("ip link delete {}".format(rif), warn=False)
904 if rc:
905 self.logger.error(
906 "Error deleting veth pair %s: %s", lname, cmd_error(rc, o, e)
907 )
908
909 self.links = {}
910
911 for host in self.hosts.values():
912 try:
913 host.delete()
914 except Exception as error:
915 self.logger.error(
916 "%s: error while deleting host %s: %s", self, host, error
917 )
918
919 self.hosts = {}
920
921 for switch in self.switches.values():
922 try:
923 switch.delete()
924 except Exception as error:
925 self.logger.error(
926 "%s: error while deleting switch %s: %s", self, switch, error
927 )
928 self.switches = {}
929
930 self.logger.debug("%s: Deleted.", self)
931
932 super(Micronet, self).delete()
933
934
935 # ---------------------------
936 # Root level utility function
937 # ---------------------------
938
939
940 def get_exec_path(binary):
941 base = Commander("base")
942 return base.get_exec_path(binary)
943
944
945 commander = Commander("micronet")