#! @PYTHON@ # Copyright (c) 2009 Nicira Networks. -*- python -*- # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import errno import fcntl import fnmatch import getopt import os import re import socket import stat import sys import syslog argv0 = sys.argv[0] if argv0.find('/') >= 0: argv0 = argv0[argv0.rfind('/') + 1:] DEFAULT_VSWITCHD_CONF = "@sysconfdir@/ovs-vswitchd.conf" vswitchd_conf = DEFAULT_VSWITCHD_CONF DEFAULT_VSWITCHD_TARGET = "ovs-vswitchd" vswitchd_target = DEFAULT_VSWITCHD_TARGET reload_vswitchd = True enable_syslog = True class Error(Exception): def __init__(self, msg): Exception.__init__(self) self.msg = msg def log(message): if enable_syslog: syslog.syslog(message) # XXX Most of the functions below should be integrated into a # VSwitchConfiguration object with logically named fields and methods # instead of this mishmash of functionality. # Locks 'filename' for writing. def cfg_lock(filename): if filename == '-': return if '/' in filename: lastSlash = filename.rfind('/') prefix = filename[:lastSlash] suffix = filename[lastSlash + 1:] lock_name = "%s/.%s.~lock~" % (prefix, suffix) else: lock_name = ".%s.~lock~" % filename while True: # Try to open an existing lock file. try: f = open(lock_name, 'r') except IOError, e: if e.errno != errno.ENOENT: raise # Try to create a new lock file. try: fd = os.open(lock_name, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0600) except OSError, e: if e.errno != errno.EEXIST: raise # Someone else created the lock file, try again. os.close(fd) continue fcntl.flock(f, fcntl.LOCK_EX) return # Read the ovs-vswitchd.conf file named 'filename' and return its contents as a # dictionary that maps from string keys to lists of string values. (Even # singleton values are represented as lists.) def cfg_read(filename, lock=False): if lock: cfg_lock(filename) try: if filename == '-': f = open('/dev/stdin') else: f = open(filename) except IOError, e: sys.stderr.write("%s: could not open %s (%s)\n" % (argv0, filename, e.strerror)) sys.exit(1) cfg = {} rx = re.compile('([-._@$:+a-zA-Z0-9]+)(?:[ \t\r\n\v]*)=(?:[ \t\r\n\v]*)(.*)$') for line in f: line = line.strip() if len(line) == 0 or line[0] == '#': continue match = rx.match(line) if match == None: continue key, value = match.groups() if key not in cfg: cfg[key] = [] cfg[key].append(value) global orig_cfg orig_cfg = cfg_clone(cfg) return cfg # Returns a deep copy of 'cfg', which must be in the format returned # by cfg_read(). def cfg_clone(cfg): new = {} for key in cfg: new[key] = list(cfg[key]) return new # Returns a list of all the configuration lines that are in 'a' but # not in 'b'. def cfg_subtract(a, b): difference = [] for key in a: for value in a[key]: if key not in b or value not in b[key]: difference.append("%s=%s" % (key, value)) return difference def do_cfg_save(cfg, file): # Log changes. added = cfg_subtract(cfg, orig_cfg) removed = cfg_subtract(orig_cfg, cfg) if added or removed: log("configuration changes:") for line in removed: log("-%s\n" % line) for line in added: log("+%s\n" % line) # Write changes. for key in sorted(cfg.keys()): for value in sorted(cfg[key]): file.write("%s=%s\n" % (key, value)) def cfg_reload(): target = VSWITCHD_TARGET if not target.startswith('/'): pid = read_first_line_of_file('%s/%s.pid' % ('@RUNDIR@', target)) target = '%s/%s.%s.ctl' % ('@RUNDIR@', target, pid) s = os.stat(target) if not stat.S_ISSOCK(s.st_mode): raise Error("%s is not a Unix domain socket, cannot reload" % target) skt = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) skt.connect(target) f = os.fdopen(skt.fileno(), "r+") f.write("vswitchd/reload\n") f.flush() f.readline() f.close() def cfg_save(cfg, filename): if filename == '-': do_cfg_save(cfg, sys.stdout) else: tmp_name = filename + ".~tmp~" f = open(tmp_name, 'w') do_cfg_save(cfg, f) f.close() os.rename(tmp_name, filename) if reload_vswitchd: cfg_reload() # Returns a set of the immediate subsections of 'section' within 'cfg'. For # example, if 'section' is "bridge" and keys bridge.a, bridge.b, bridge.b.c, # and bridge.c.x.y.z exist, returns set(['a', 'b', 'c']). def cfg_get_subsections(cfg, section): subsections = set() for key in cfg: if key.startswith(section + "."): dot = key.find(".", len(section) + 1) if dot == -1: dot = len(key) subsections.add(key[len(section) + 1:dot]) return subsections # Returns True if 'cfg' contains a key whose single value is 'true'. Otherwise # returns False. def cfg_get_bool(cfg, name): return name in cfg and cfg[name] == ['true'] # If 'cfg' has a port named 'port' configured with an implicit VLAN, returns # that VLAN number. Otherwise, returns 0. def get_port_vlan(cfg, port): try: return int(cfg["vlan.%s.tag" % port][0]) except (ValueError, KeyError): return 0 # Returns all the ports within 'bridge' in 'cfg'. If 'vlan' is nonnegative, # the ports returned are only those configured with implicit VLAN 'vlan'. def get_bridge_ports(cfg, bridge, vlan): ports = [] for port in cfg["bridge.%s.port" % bridge]: if vlan < 0 or get_port_vlan(cfg, port) == vlan: ports.append(port) return ports # Returns all the interfaces within 'bridge' in 'cfg'. If 'vlan' is # nonnegative, the interfaces returned are only those whose ports are # configured with implicit VLAN 'vlan'. def get_bridge_ifaces(cfg, bridge, vlan): ifaces = [] for port in get_bridge_ports(cfg, bridge, vlan): ifaces.extend(cfg.get("bonding.%s.slave" % port, [port])) return ifaces # Returns the first line of the file named 'name', with the trailing new-line # (if any) stripped off. def read_first_line_of_file(name): file = None try: file = open(name, 'r') return file.readline().rstrip('\n') finally: if file != None: file.close() # Returns a bridge ID constructed from the MAC address of network device # 'netdev', in the format "8000.000102030405". def get_bridge_id(netdev): try: hwaddr = read_first_line_of_file("/sys/class/net/%s/address" % netdev) return "8000.%s" % (hwaddr.replace(":", "")) except: return "8000.002320ffffff" # Returns a list of 3-tuples based on 'cfg'. Each 3-tuple represents # one real bridge or one fake bridge and has the form (bridge, parent, # vlan), where 'bridge' is the real or fake bridge name, 'parent' is # the same as 'bridge' for a real bridge or the name of the containing # bridge for a fake bridge, and 'vlan' is 0 for a real bridge or a # VLAN number for a fake bridge. def get_bridge_info(cfg): real_bridges = [(br, br, 0) for br in get_real_bridges(cfg)] fake_bridges = [] for linux_bridge, ovs_bridge, vlan in real_bridges: for iface in get_bridge_ifaces(cfg, ovs_bridge, -1): if cfg_get_bool(cfg, "iface.%s.fake-bridge" % iface): fake_bridges.append((iface, ovs_bridge, get_port_vlan(cfg, iface))) return real_bridges + fake_bridges # Returns the real bridges configured in 'cfg'. def get_real_bridges(cfg): return cfg_get_subsections(cfg, "bridge") # Returns the fake bridges configured in 'cfg'. def get_fake_bridges(cfg): return [bridge for bridge, parent, vlan in get_bridge_info(cfg) if bridge != parent] # Returns all the real and fake bridges configured in 'cfg'. def get_all_bridges(cfg): return [bridge for bridge, parent, vlan in get_bridge_info(cfg)] # Returns the parent bridge and VLAN of real or fake 'bridge' in # 'cfg', where the parent bridge and VLAN are as defined in the # description of get_bridge_info(). Raises an error if no bridge # named 'bridge' exists in 'cfg'. def find_bridge(cfg, bridge): for br, parent, vlan in get_bridge_info(cfg): if br == bridge: return parent, vlan raise Error("no bridge named %s" % bridge) def del_matching_keys(cfg, pattern): for key in [key for key in cfg.keys() if fnmatch.fnmatch(key, pattern)]: del cfg[key] # Deletes anything related to a port named 'port' from 'cfg'. No port # named 'port' need actually exist; this function will clean up # regardless. def del_port(cfg, port): # The use of [!0-9] keeps an interface of 'eth0' from matching # VLANs attached to eth0 (such as 'eth0.123'), which are distinct # interfaces. for iface in cfg.get('bonding.%s.slave' % port, [port]): del_matching_keys(cfg, 'iface.%s.[!0-9]*' % iface) # Yes, this "port" setting applies to interfaces, not ports, *sigh*. del_matching_keys(cfg, 'port.%s.ingress-policing*' % iface) del_matching_keys(cfg, 'bonding.%s.[!0-9]*' % port) del_matching_keys(cfg, 'vlan.%s.[!0-9]*' % port) for key in cfg.keys(): if fnmatch.fnmatch(key, 'bridge.*.port'): cfg[key] = [s for s in cfg[key] if s != port] # Returns the name of the (real or fake) bridge in 'cfg' that contains # port 'port', or None if there is no such port. def port_to_bridge(cfg, port): for bridge, parent, vlan in get_bridge_info(cfg): if port != bridge and port in get_bridge_ports(cfg, parent, vlan): return bridge return None def usage(): print """%(argv0)s: ovs-vswitchd management utility usage: %(argv0)s [OPTIONS] COMMAND [ARG...] Bridge commands: add-br BRIDGE create a new bridge named BRIDGE add-br BRIDGE PARENT VLAN create new fake bridge BRIDGE in PARENT on VLAN del-br BRIDGE delete BRIDGE and all of its ports list-br print the names of all the bridges br-exists BRIDGE test whether BRIDGE exists br-to-vlan BRIDGE print the VLAN which BRIDGE is on br-to-parent BRIDGE print the parent of BRIDGE Port commands: list-ports BRIDGE print the names of all the ports on BRIDGE add-port BRIDGE PORT add network device PORT to BRIDGE add-bond BRIDGE PORT IFACE... add new bonded port PORT in BRIDGE from IFACES del-port [BRIDGE] PORT delete PORT (which may be bonded) from BRIDGE port-to-br PORT print name of bridge that contains PORT A bond is considered to be a single port. Interface commands (a bond consists of multiple interfaces): list-ifaces BRIDGE print the names of all the interfaces on BRIDGE iface-to-br IFACE print name of bridge that contains IFACE A bond is considered to consist of interfaces. General options: --no-syslog do not write mesages to syslog -c, --config=FILE set configuration file (default: %(config)s) -t, --target=PROGRAM|SOCKET set ovs-vswitchd target (default: %(target)s) --no-reload do not make ovs-vswitchd reload its configuration -h, --help display this help message and exit -V, --version display version information and exit Report bugs to bugs@openvswitch.org.""" % {'argv0': argv0, 'config': DEFAULT_VSWITCHD_CONF, 'target': DEFAULT_VSWITCHD_TARGET} sys.exit(0) def version(): print "ovs-vsctl (Open vSwitch) @VERSION@" sys.exit(0) def check_conflicts(cfg, name, op): bridges = get_bridge_info(cfg) if name in [bridge for bridge, parent, vlan in bridges]: raise Error("%s because a bridge named %s already exists" % (op, name)) for bridge, parent, vlan in bridges: if name in get_bridge_ports(cfg, parent, vlan): raise Error("%s because a port named %s already exists on bridge %s" % (op, name, bridge)) if name in get_bridge_ifaces(cfg, parent, vlan): raise Error("%s because an interface named %s already exists on bridge %s" % (op, name, bridge)) def cmd_add_br(cfg, bridge, parent=None, vlan=None): check_conflicts(cfg, bridge, "cannot create a bridge named %s" % bridge) if parent and vlan: if parent in get_fake_bridges(cfg): raise Error("cannot create bridge with fake bridge as parent") if parent not in get_real_bridges(cfg): raise Error("parent bridge %s does not exist" % bridge) try: if int(vlan) < 0 or int(vlan) > 4095: raise ValueError except ValueError: raise Error("invalid VLAN number %s" % vlan) # Create fake bridge internal port. cfg['iface.%s.internal' % bridge] = ['true'] cfg['iface.%s.fake-bridge' % bridge] = ['true'] cfg['vlan.%s.tag' % bridge] = [vlan] # Add fake bridge port to parent. cfg['bridge.%s.port' % parent].append(bridge) else: cfg['bridge.%s.port' % bridge] = [bridge] def cmd_del_br(cfg, bridge): parent, vlan = find_bridge(cfg, bridge) if vlan == 0: vlan = -1 for port in set(get_bridge_ports(cfg, parent, vlan) + [bridge]): del_port(cfg, port) if vlan < 0: del_matching_keys(cfg, 'bridge.%s.[!0-9]*' % bridge) def cmd_list_br(cfg): return get_all_bridges(cfg) def cmd_br_exists(cfg, bridge): if bridge not in get_all_bridges(cfg): sys.exit(2) def cmd_list_ports(cfg, bridge): ports = [] parent, vlan = find_bridge(cfg, bridge) for port in get_bridge_ports(cfg, parent, vlan): if port != bridge: ports.append(port) return ports def do_add_port(cfg, bridge, parent, port, vlan): check_conflicts(cfg, port, "cannot create a port named %s" % port) cfg['bridge.%s.port' % parent].append(port) if vlan > 0: cfg['vlan.%s.tag' % port] = [vlan] def cmd_add_port(cfg, bridge, port): parent, vlan = find_bridge(cfg, bridge) do_add_port(cfg, bridge, parent, port, vlan) def cmd_add_bond(cfg, bridge, port, *slaves): parent, vlan = find_bridge(cfg, bridge) do_add_port(cfg, bridge, parent, port, vlan) cfg['bonding.%s.slave' % port] = list(slaves) def cmd_del_port(cfg, *args): if len(args) == 2: bridge, port = args parent, vlan = find_bridge(cfg, bridge) if port not in get_bridge_ports(cfg, parent, vlan): if port in get_bridge_ports(cfg, parent, -1): raise Error("bridge %s does not have a port %s (although its parent bridge %s does)" % (bridge, port, parent)) else: raise Error("bridge %s does not have a port %s" % (bridge, port)) else: port, = args if not port_to_bridge(cfg, port): raise Error("no port %s on any bridge" % port) del_port(cfg, port) def cmd_port_to_br(cfg, port): bridge = port_to_bridge(cfg, port) if bridge: return (bridge,) else: raise Error("no port named %s" % port) def cmd_list_ifaces(cfg, bridge): ifaces = [] parent, vlan = find_bridge(cfg, bridge) for iface in get_bridge_ifaces(cfg, parent, vlan): if iface != bridge: ifaces.append(iface) return ifaces def cmd_iface_to_br(cfg, iface): for bridge, parent, vlan in get_bridge_info(cfg): if iface != bridge and iface in get_bridge_ifaces(cfg, parent, vlan): return (bridge,) raise Error("no interface named %s" % iface) def cmd_br_to_vlan(cfg, bridge): parent, vlan = find_bridge(cfg, bridge) return (vlan,) def cmd_br_to_parent(cfg, bridge): parent, vlan = find_bridge(cfg, bridge) return (parent,) cmdTable = {'add-br': (cmd_add_br, True, lambda n: n == 1 or n == 3), 'del-br': (cmd_del_br, True, 1), 'list-br': (cmd_list_br, False, 0), 'br-exists': (cmd_br_exists, False, 1), 'list-ports': (cmd_list_ports, False, 1), 'add-port': (cmd_add_port, True, 2), 'add-bond': (cmd_add_bond, True, lambda n: n >= 4), 'del-port': (cmd_del_port, True, lambda n: n == 1 or n == 2), 'port-to-br': (cmd_port_to_br, False, 1), 'br-to-vlan': (cmd_br_to_vlan, False, 1), 'br-to-parent': (cmd_br_to_parent, False, 1), 'list-ifaces': (cmd_list_ifaces, False, 1), 'iface-to-br': (cmd_iface_to_br, False, 1)} # Break up commands at -- boundaries. def split_commands(args): commands = [] command = [] for arg in args: if arg == '--': if command: commands.append(command) command = [] else: command.append(arg) if command: commands.append(command) return commands def check_command(args): command, args = args[0], args[1:] if command not in cmdTable: sys.stderr.write("%s: unknown command '%s' (use --help for help)\n" % (argv0, command)) sys.exit(1) function, is_mutator, nargs = cmdTable[command] if callable(nargs) and not nargs(len(args)): sys.stderr.write("%s: '%s' command does not accept %d arguments (use --help for help)\n" % (argv0, command, len(args))) sys.exit(1) elif not callable(nargs) and len(args) != nargs: sys.stderr.write("%s: '%s' command takes %d arguments but %d were supplied (use --help for help)\n" % (argv0, command, nargs, len(args))) sys.exit(1) def run_command(cfg, args): command, args = args[0], args[1:] function, need_lock, nargs = cmdTable[command] return function(cfg, *args) def main(): # Parse command line. try: options, args = getopt.getopt(sys.argv[1:], "c:t:hV", ["config=", "target=", "no-reload", "no-syslog", "oneline", "help", "version"]) except getopt.GetoptError, msg: sys.stderr.write("%s: %s (use --help for help)\n" % (argv0, msg)) sys.exit(1) # Handle options. oneline = False for opt, optarg in options: if opt == "-c" or opt == "--config": global vswitchd_conf vswitchd_conf = optarg elif opt == "-t" or opt == "--target": global vswitchd_target vswitchd_target = optarg elif opt == "--no-reload": global reload_vswitchd reload_vswitchd = False elif opt == "-h" or opt == "--help": usage() elif opt == "-V" or opt == "--version": version() elif opt == "--no-syslog": global enable_syslog enable_syslog = False elif opt == "--oneline": oneline = True else: raise RuntimeError("unhandled option %s" % opt) if enable_syslog: syslog.openlog("ovs-vsctl") log("Called as %s" % ' '.join(sys.argv[1:])) # Break arguments into a series of commands. commands = split_commands(args) if not commands: sys.stderr.write("%s: missing command name (use --help for help)\n" % argv0) sys.exit(1) # Check command syntax. need_lock = False for command in commands: check_command(command) if cmdTable[command[0]][1]: need_lock = True # Execute commands. cfg = cfg_read(vswitchd_conf, need_lock) for command in commands: output = run_command(cfg, command) if oneline: if output == None: output = () print '\\n'.join([str(s).replace('\\', '\\\\') for s in output]) elif output != None: for line in output: print line if need_lock: cfg_save(cfg, vswitchd_conf) sys.exit(0) if __name__ == "__main__": try: main() except Error, msg: sys.stderr.write("%s: %s\n" % (argv0, msg.msg)) sys.exit(1)