mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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
375 lines
14 KiB
Python
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" |