cloudstack/scripts/vm/hypervisor/xenserver/cloudstack_pluginlib.py
Murali Reddy 100df92455 Scripts that use ovs-vsctl and ovs-ofctl to setup a bridge for VPC in
distributed routing mode, and setup flows appropriatley

script to handle the VPC topology sent from management server in JSOn
format. From the JSON file, reqired configuration (tunnel setup and flow
rules setup) is setup on the bridge
2014-03-14 16:56:36 +05:30

375 lines
14 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# Common function for Cloudstack's XenAPI plugins
import ConfigParser
import logging
import os
import subprocess
import json
from time import localtime, asctime
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_LOG_FILE = "/var/log/cloudstack_plugins.log"
PLUGIN_CONFIG_PATH = "/etc/xensource/cloudstack_plugins.conf"
OVSDB_PID_PATH = "/var/run/openvswitch/ovsdb-server.pid"
OVSDB_DAEMON_PATH = "ovsdb-server"
OVS_PID_PATH = "/var/run/openvswitch/ovs-vswitchd.pid"
OVS_DAEMON_PATH = "ovs-vswitchd"
VSCTL_PATH = "/usr/bin/ovs-vsctl"
OFCTL_PATH = "/usr/bin/ovs-ofctl"
XE_PATH = "/opt/xensource/bin/xe"
class PluginError(Exception):
"""Base Exception class for all plugin errors."""
def __init__(self, *args):
Exception.__init__(self, *args)
def setup_logging(log_file=None):
debug = False
verbose = False
log_format = DEFAULT_LOG_FORMAT
log_date_format = DEFAULT_LOG_DATE_FORMAT
# try to read plugin configuration file
if os.path.exists(PLUGIN_CONFIG_PATH):
config = ConfigParser.ConfigParser()
config.read(PLUGIN_CONFIG_PATH)
try:
options = config.options('LOGGING')
if 'debug' in options:
debug = config.getboolean('LOGGING', 'debug')
if 'verbose' in options:
verbose = config.getboolean('LOGGING', 'verbose')
if 'format' in options:
log_format = config.get('LOGGING', 'format')
if 'date_format' in options:
log_date_format = config.get('LOGGING', 'date_format')
if 'file' in options:
log_file_2 = config.get('LOGGING', 'file')
except ValueError:
# configuration file contained invalid attributes
# ignore them
pass
except ConfigParser.NoSectionError:
# Missing 'Logging' section in configuration file
pass
root_logger = logging.root
if debug:
root_logger.setLevel(logging.DEBUG)
elif verbose:
root_logger.setLevel(logging.INFO)
else:
root_logger.setLevel(logging.WARNING)
formatter = logging.Formatter(log_format, log_date_format)
log_filename = log_file or log_file_2 or DEFAULT_LOG_FILE
logfile_handler = logging.FileHandler(log_filename)
logfile_handler.setFormatter(formatter)
root_logger.addHandler(logfile_handler)
def do_cmd(cmd):
"""Abstracts out the basics of issuing system commands. If the command
returns anything in stderr, a PluginError is raised with that information.
Otherwise, the output from stdout is returned.
"""
pipe = subprocess.PIPE
logging.debug("Executing:%s", cmd)
proc = subprocess.Popen(cmd, shell=False, stdin=pipe, stdout=pipe,
stderr=pipe, close_fds=True)
ret_code = proc.wait()
err = proc.stderr.read()
if ret_code:
logging.debug("The command exited with the error code: " +
"%s (stderr output:%s)" % (ret_code, err))
raise PluginError(err)
output = proc.stdout.read()
if output.endswith('\n'):
output = output[:-1]
return output
def _is_process_run(pidFile, name):
try:
fpid = open(pidFile, "r")
pid = fpid.readline()
fpid.close()
except IOError, e:
return -1
pid = pid[:-1]
ps = os.popen("ps -ae")
for l in ps:
if pid in l and name in l:
ps.close()
return 0
ps.close()
return -2
def _is_tool_exist(name):
if os.path.exists(name):
return 0
return -1
def check_switch():
global result
ret = _is_process_run(OVSDB_PID_PATH, OVSDB_DAEMON_PATH)
if ret < 0:
if ret == -1:
return "NO_DB_PID_FILE"
if ret == -2:
return "DB_NOT_RUN"
ret = _is_process_run(OVS_PID_PATH, OVS_DAEMON_PATH)
if ret < 0:
if ret == -1:
return "NO_SWITCH_PID_FILE"
if ret == -2:
return "SWITCH_NOT_RUN"
if _is_tool_exist(VSCTL_PATH) < 0:
return "NO_VSCTL"
if _is_tool_exist(OFCTL_PATH) < 0:
return "NO_OFCTL"
return "SUCCESS"
def _build_flow_expr(**kwargs):
is_delete_expr = kwargs.get('delete', False)
flow = ""
if not is_delete_expr:
flow = "hard_timeout=%s,idle_timeout=%s,priority=%s"\
% (kwargs.get('hard_timeout', '0'),
kwargs.get('idle_timeout', '0'),
kwargs.get('priority', '1'))
in_port = 'in_port' in kwargs and ",in_port=%s" % kwargs['in_port'] or ''
dl_type = 'dl_type' in kwargs and ",dl_type=%s" % kwargs['dl_type'] or ''
dl_src = 'dl_src' in kwargs and ",dl_src=%s" % kwargs['dl_src'] or ''
dl_dst = 'dl_dst' in kwargs and ",dl_dst=%s" % kwargs['dl_dst'] or ''
nw_src = 'nw_src' in kwargs and ",nw_src=%s" % kwargs['nw_src'] or ''
nw_dst = 'nw_dst' in kwargs and ",nw_dst=%s" % kwargs['nw_dst'] or ''
table = 'table' in kwargs and ",table=%s" % kwargs['table'] or ''
proto = 'proto' in kwargs and ",%s" % kwargs['proto'] or ''
ip = ('nw_src' in kwargs or 'nw_dst' in kwargs) and ',ip' or ''
flow = (flow + in_port + dl_type + dl_src + dl_dst +
(ip or proto) + nw_src + nw_dst)
return flow
def add_flow(bridge, **kwargs):
"""
Builds a flow expression for **kwargs and adds the flow entry
to an Open vSwitch instance
"""
flow = _build_flow_expr(**kwargs)
actions = 'actions' in kwargs and ",actions=%s" % kwargs['actions'] or ''
flow = flow + actions
addflow = [OFCTL_PATH, "add-flow", bridge, flow]
do_cmd(addflow)
def del_flows(bridge, **kwargs):
"""
Removes flows according to criteria passed as keyword.
"""
flow = _build_flow_expr(delete=True, **kwargs)
# out_port condition does not exist for all flow commands
out_port = ("out_port" in kwargs and
",out_port=%s" % kwargs['out_port'] or '')
flow = flow + out_port
delFlow = [OFCTL_PATH, 'del-flows', bridge, flow]
do_cmd(delFlow)
def del_all_flows(bridge):
delFlow = [OFCTL_PATH, "del-flows", bridge]
do_cmd(delFlow)
normalFlow = "priority=0 idle_timeout=0 hard_timeout=0 actions=normal"
add_flow(bridge, normalFlow)
def del_port(bridge, port):
delPort = [VSCTL_PATH, "del-port", bridge, port]
do_cmd(delPort)
def get_network_id_for_vif(vif_name):
domain_id, device_id = vif_name[3:len(vif_name)].split(".")
dom_uuid = do_cmd([XE_PATH, "vm-list", "dom-id=%s" % domain_id, "--minimal"])
vif_uuid = do_cmd([XE_PATH, "vif-list", "vm-uuid=%s" % dom_uuid, "device=%s" % device_id, "--minimal"])
vnet = do_cmd([XE_PATH, "vif-param-get", "uuid=%s" % vif_uuid, "param-name=other-config",
"param-key=cloudstack-network-id"])
return vnet
def get_network_id_for_tunnel_port(tunnelif_name):
vnet = do_cmd([VSCTL_PATH, "get", "interface", tunnelif_name, "options:cloudstack-network-id"])
return vnet
def clear_flooding_rules_for_port(bridge, ofport):
del_flows(bridge, in_port=ofport, table=2)
def add_flooding_rules_for_port(bridge, in_ofport, out_ofports):
action = "".join("output:%s," %ofport for ofport in out_ofports)[:-1]
add_flow(bridge, priority=1100, in_port=in_ofport, table=1, actions=action)
def get_ofport_for_vif(vif_name):
return do_cmd([VSCTL_PATH, "get", "interface", vif_name, "ofport"])
def get_macaddress_of_vif(vif_name):
domain_id, device_id = vif_name[3:len(vif_name)].split(".")
dom_uuid = do_cmd([XE_PATH, "vm-list", "dom-id=%s" % domain_id, "--minimal"])
vif_uuid = do_cmd([XE_PATH, "vif-list", "vm-uuid=%s" % dom_uuid, "device=%s" % device_id, "--minimal"])
mac = do_cmd([XE_PATH, "vif-param-get", "uuid=%s" % vif_uuid, "param-name=MAC"])
return mac
def get_vif_name_from_macaddress(macaddress):
vif_uuid = do_cmd([XE_PATH, "vif-list", "MAC=%s" % macaddress, "--minimal"])
vif_device_id = do_cmd([XE_PATH, "vif-param-get", "uuid=%s" % vif_uuid, "param-name=device"])
vm_uuid = do_cmd([XE_PATH, "vif-param-get", "uuid=%s" % vif_uuid, "param-name=vm-uuid"])
vm_domain_id = do_cmd([XE_PATH, "vm-param-get", "uuid=%s" % vm_uuid, "param-name=dom-id"])
return "vif"+vm_domain_id+"."+vif_device_id
def add_mac_lookup_table_entry(bridge, mac_address, out_of_port):
add_flow(bridge, priority=1100, dl_dst=mac_address, table=1, actions="output:%s" % out_of_port)
def delete_mac_lookup_table_entry(bridge, mac_address):
del_flows(bridge, dl_dst=mac_address, table=1)
def add_ip_lookup_table_entry(bridge, ip, dst_tier_gateway_mac, dst_vm_mac):
action_str = "mod_dl_sr:%s" % dst_tier_gateway_mac + ",mod_dl_dst:%s" % dst_vm_mac +",resubmit(,5)"
addflow = [OFCTL_PATH, "add-flow", bridge, "table=4", "nw_dst=%s" % ip, "actions=%s" %action_str]
do_cmd(addflow)
def get_vms_on_host(vpc, host_id):
all_vms = vpc.vms
vms_on_host = []
for vm in all_vms:
if vm.hostid == host_id:
vms_on_host.append(vm)
return vms_on_host
def get_network_details(vpc, network_uuid):
tiers = vpc.tiers
for tier in tiers:
if tier.networkuuid == network_uuid:
return tier
return None
class jsonLoader(object):
def __init__(self, obj):
for k in obj:
v = obj[k]
if isinstance(v, dict):
setattr(self, k, jsonLoader(v))
elif isinstance(v, (list, tuple)):
if len(v) > 0 and isinstance(v[0], dict):
setattr(self, k, [jsonLoader(elem) for elem in v])
else:
setattr(self, k, v)
else:
setattr(self, k, v)
def __getattr__(self, val):
if val in self.__dict__:
return self.__dict__[val]
else:
return None
def __repr__(self):
return '{%s}' % str(', '.join('%s : %s' % (k, repr(v)) for (k, v)
in self.__dict__.iteritems()))
def __str__(self):
return '{%s}' % str(', '.join('%s : %s' % (k, repr(v)) for (k, v)
in self.__dict__.iteritems()))
def configure_bridge_for_topology(bridge, this_host_id, json_config):
vpconfig = jsonLoader(json.loads(json_config)).vpc
if vpconfig is None:
logging.debug("WARNING:Can't find VPC info in json config file")
return "FAILURE:IMPROPER_JSON_CONFG_FILE"
# get the list of Vm's in the VPC from the JSON config
this_host_vms = get_vms_on_host(vpconfig, this_host_id)
for vm in this_host_vms:
for nic in vm.nics:
mac_addr = nic.macaddress
ip = nic.ipaddress
vif_name = get_vif_name_from_macaddress(mac_addr)
of_port = get_ofport_for_vif(vif_name)
network = get_network_details(vpconfig, nic.networkuuid)
# Add flow rule in L2 look up table, if the destination mac = MAC of the nic send packet on the found OFPORT
add_mac_lookup_table_entry(bridge, mac_addr, of_port)
# Add flow rule in L3 look up table: if the destination IP = VM's IP then modify the packet
# to set DST MAC = VM's MAC, SRC MAC=tier gateway MAC and send to egress table
add_ip_lookup_table_entry(bridge, ip, network.gatewaymac, mac_addr)
# Add flow entry to send with intra tier traffic from the NIC to L2 lookup path)
addflow = [OFCTL_PATH, "add-flow", bridge, "table=0", "in_port=%s" % of_port,
"nw_dst=%s" %network.cidr, "actions=resubmit(,1)"]
do_cmd(addflow)
#add flow entry to send inter-tier traffic from the NIC to egress ACL table(to L3 lookup path)
addflow = [OFCTL_PATH, "add-flow", bridge, "table=0", "in_port=%s" % of_port,
"dl_dst=%s" %network.gatewaymac, "nw_dst=%s" %vpconfig.cidr, "actions=resubmit(,3)"]
do_cmd(addflow)
# get the list of hosts on which VPC spans from the JSON config
vpc_spanning_hosts = vpconfig.hosts
for host in vpc_spanning_hosts:
if this_host_id == host.hostid:
continue
other_host_vms = get_vms_on_host(vpconfig, host.hostid)
for vm in other_host_vms:
for nic in vm.nics:
mac_addr = nic.macaddress
ip = nic.ipaddress
network = get_network_details(vpconfig, nic.networkuuid)
gre_key = network.grekey
# generate tunnel name from tunnel naming convention
tunnel_name = "t%s-%s-%s" % (gre_key, this_host_id, host.hostid)
of_port = get_ofport_for_vif(tunnel_name)
# Add flow rule in L2 look up table, if the destination mac = MAC of the nic send packet tunnel port
add_mac_lookup_table_entry(bridge, mac_addr, of_port)
# Add flow tule in L3 look up table: if the destination IP = VM's IP then modify the packet
# set DST MAC = VM's MAC, SRC MAC=tier gateway MAC and send to egress table
add_ip_lookup_table_entry(bridge, ip, network.gatewaymac, mac_addr)
return "SUCCESS: successfully configured bridge as per the VPC toplogy"