Merge "Fuel plugins: Add support for using neutron vlan network"
This commit is contained in:
commit
cfa8c609ac
|
@ -1,15 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import ConfigParser
|
||||
import logging
|
||||
import netifaces
|
||||
import os
|
||||
from logging import debug, info, warning, DEBUG, basicConfig
|
||||
from subprocess import Popen, PIPE
|
||||
import yaml
|
||||
from shutil import rmtree
|
||||
from tempfile import mkstemp, mkdtemp
|
||||
import re
|
||||
from socket import inet_ntoa
|
||||
from struct import pack
|
||||
import netifaces
|
||||
import re
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
|
||||
ASTUTE_PATH = '/etc/astute.yaml'
|
||||
|
@ -17,22 +16,27 @@ ASTUTE_SECTION = 'fuel-plugin-xenserver'
|
|||
LOG_ROOT = '/var/log/fuel-plugin-xenserver'
|
||||
LOG_FILE = 'compute_post_deployment.log'
|
||||
HIMN_IP = '169.254.0.1'
|
||||
INT_BRIDGE = 'br-int'
|
||||
XS_PLUGIN_ISO = 'xenserverplugins-liberty.iso'
|
||||
DIST_PACKAGES_DIR = '/usr/lib/python2.7/dist-packages/'
|
||||
|
||||
if not os.path.exists(LOG_ROOT):
|
||||
os.mkdir(LOG_ROOT)
|
||||
|
||||
basicConfig(filename=os.path.join(LOG_ROOT, LOG_FILE), level=DEBUG)
|
||||
logging.basicConfig(filename=os.path.join(LOG_ROOT, LOG_FILE),
|
||||
level=logging.DEBUG)
|
||||
|
||||
|
||||
def reportError(err):
|
||||
warning(err)
|
||||
logging.warning(err)
|
||||
raise Exception(err)
|
||||
|
||||
|
||||
def execute(*cmd, **kwargs):
|
||||
cmd = map(str, cmd)
|
||||
info(' '.join(cmd))
|
||||
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
logging.info(' '.join(cmd))
|
||||
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
if 'prompt' in kwargs:
|
||||
prompt = kwargs.get('prompt')
|
||||
|
@ -45,7 +49,7 @@ def execute(*cmd, **kwargs):
|
|||
(out, err) = (out.replace('\n', ''), err.replace('\n', ''))
|
||||
|
||||
if out:
|
||||
debug(out)
|
||||
logging.debug(out)
|
||||
|
||||
if proc.returncode is not None and proc.returncode != 0:
|
||||
reportError(err)
|
||||
|
@ -91,13 +95,13 @@ def astute_get(dct, keys, default=None, fail_if_missing=True):
|
|||
|
||||
def get_options(astute, astute_section):
|
||||
"""Return username and password filled in plugin."""
|
||||
if not astute_section in astute:
|
||||
if astute_section not in astute:
|
||||
reportError('%s not found' % astute_section)
|
||||
|
||||
options = astute[astute_section]
|
||||
info('username: {username}'.format(**options))
|
||||
info('password: {password}'.format(**options))
|
||||
info('install_xapi: {install_xapi}'.format(**options))
|
||||
logging.info('username: {username}'.format(**options))
|
||||
logging.info('password: {password}'.format(**options))
|
||||
logging.info('install_xapi: {install_xapi}'.format(**options))
|
||||
return options['username'], options['password'], \
|
||||
options['install_xapi']
|
||||
|
||||
|
@ -112,8 +116,8 @@ def get_endpoints(astute):
|
|||
endpoints[k]['IP'][0]
|
||||
) for k in endpoints])
|
||||
|
||||
info('storage network: {storage}'.format(**endpoints))
|
||||
info('mgmt network: {mgmt}'.format(**endpoints))
|
||||
logging.info('storage network: {storage}'.format(**endpoints))
|
||||
logging.info('mgmt network: {mgmt}'.format(**endpoints))
|
||||
return endpoints
|
||||
|
||||
|
||||
|
@ -128,7 +132,7 @@ def init_eth():
|
|||
himn_mac = execute(
|
||||
'xenstore-read',
|
||||
'/local/domain/%s/vm-data/himn_mac' % domid)
|
||||
info('himn_mac: %s' % himn_mac)
|
||||
logging.info('himn_mac: %s' % himn_mac)
|
||||
|
||||
_mac = lambda eth: \
|
||||
netifaces.ifaddresses(eth).get(netifaces.AF_LINK)[0]['addr']
|
||||
|
@ -137,7 +141,7 @@ def init_eth():
|
|||
reportError('Cannot find eth matches himn_mac')
|
||||
|
||||
eth = eths[0]
|
||||
info('himn_eth: %s' % eth)
|
||||
logging.info('himn_eth: %s' % eth)
|
||||
|
||||
ip = netifaces.ifaddresses(eth).get(netifaces.AF_INET)
|
||||
|
||||
|
@ -149,7 +153,7 @@ def init_eth():
|
|||
'post-up route del default dev {eth}').format(eth=eth)
|
||||
with open(fname, 'w') as f:
|
||||
f.write(s)
|
||||
info('%s created' % fname)
|
||||
logging.info('%s created' % fname)
|
||||
execute('ifdown', eth)
|
||||
execute('ifup', eth)
|
||||
ip = netifaces.ifaddresses(eth).get(netifaces.AF_INET)
|
||||
|
@ -158,7 +162,7 @@ def init_eth():
|
|||
himn_local = ip[0]['addr']
|
||||
himn_xs = '.'.join(himn_local.split('.')[:-1] + ['1'])
|
||||
if HIMN_IP == himn_xs:
|
||||
info('himn_local: %s' % himn_local)
|
||||
logging.info('himn_local: %s' % himn_local)
|
||||
return eth, himn_local
|
||||
|
||||
reportError('HIMN failed to get IP address from XenServer')
|
||||
|
@ -173,23 +177,11 @@ def check_hotfix_exists(himn, username, password, hotfix):
|
|||
|
||||
def install_xenapi_sdk():
|
||||
"""Install XenAPI Python SDK"""
|
||||
execute('cp', 'XenAPI.py', '/usr/lib/python2.7/dist-packages/')
|
||||
execute('cp', 'XenAPI.py', DIST_PACKAGES_DIR)
|
||||
|
||||
|
||||
def create_novacompute_conf(himn, username, password, public_ip):
|
||||
"""Fill nova-compute.conf with HIMN IP and root password. """
|
||||
template = '\n'.join([
|
||||
'[DEFAULT]',
|
||||
'compute_driver=xenapi.XenAPIDriver',
|
||||
'force_config_drive=True',
|
||||
'novncproxy_base_url=https://%s:6080/vnc_auto.html',
|
||||
'vncserver_proxyclient_address=%s',
|
||||
'[xenserver]',
|
||||
'connection_url=http://%s',
|
||||
'connection_username="%s"',
|
||||
'connection_password="%s"'
|
||||
])
|
||||
|
||||
mgmt_if = netifaces.ifaddresses('br-mgmt')
|
||||
if mgmt_if and mgmt_if.get(netifaces.AF_INET) \
|
||||
and mgmt_if.get(netifaces.AF_INET)[0]['addr']:
|
||||
|
@ -197,19 +189,27 @@ def create_novacompute_conf(himn, username, password, public_ip):
|
|||
else:
|
||||
reportError('Cannot get IP Address on Management Network')
|
||||
|
||||
s = template % (public_ip, mgmt_ip, himn, username, password)
|
||||
fname = '/etc/nova/nova-compute.conf'
|
||||
with open(fname, 'w') as f:
|
||||
f.write(s)
|
||||
info('%s created' % fname)
|
||||
|
||||
|
||||
def restart_nova_services():
|
||||
"""Restart nova services"""
|
||||
execute('stop', 'nova-compute')
|
||||
execute('start', 'nova-compute')
|
||||
execute('stop', 'nova-network')
|
||||
execute('start', 'nova-network')
|
||||
filename = '/etc/nova/nova-compute.conf'
|
||||
cf = ConfigParser.ConfigParser()
|
||||
try:
|
||||
cf.read(filename)
|
||||
cf.set('DEFAULT', 'compute_driver', 'xenapi.XenAPIDriver')
|
||||
cf.set('DEFAULT', 'force_config_drive', 'True')
|
||||
cf.set('DEFAULT', 'novncproxy_base_url',
|
||||
'https://%s:6080/vnc_auto.html' % public_ip)
|
||||
cf.set('DEFAULT', 'vncserver_proxyclient_address', mgmt_ip)
|
||||
if not cf.has_section('xenserver'):
|
||||
cf.add_section('xenserver')
|
||||
cf.set('xenserver', 'connection_url', 'http://%s' % himn)
|
||||
cf.set('xenserver', 'connection_username', username)
|
||||
cf.set('xenserver', 'connection_password', password)
|
||||
cf.set('xenserver', 'vif_driver',
|
||||
'nova.virt.xenapi.vif.XenAPIOpenVswitchDriver')
|
||||
cf.set('xenserver', 'ovs_integration_bridge', INT_BRIDGE)
|
||||
cf.write(open(filename, 'w'))
|
||||
except Exception:
|
||||
reportError('Cannot set configurations to %s' % filename)
|
||||
logging.info('%s created' % filename)
|
||||
|
||||
|
||||
def route_to_compute(endpoints, himn_xs, himn_local, username, password):
|
||||
|
@ -238,17 +238,17 @@ def route_to_compute(endpoints, himn_xs, himn_local, username, password):
|
|||
% ' '.join(params)
|
||||
ssh(himn_xs, username, password, sh)
|
||||
else:
|
||||
info('%s network ip is missing' % endpoint_name)
|
||||
logging.info('%s network ip is missing' % endpoint_name)
|
||||
|
||||
|
||||
def install_suppack(himn, username, password):
|
||||
"""Install xapi driver supplemental pack. """
|
||||
# TODO: check if installed
|
||||
scp(himn, username, password, '/tmp/', 'novaplugins-kilo.iso')
|
||||
out = ssh(
|
||||
# TODO(Johnhua): check if installed
|
||||
scp(himn, username, password, '/tmp/', XS_PLUGIN_ISO)
|
||||
ssh(
|
||||
himn, username, password, 'xe-install-supplemental-pack',
|
||||
'/tmp/novaplugins-kilo.iso', prompt='Y\n')
|
||||
ssh(himn, username, password, 'rm', '/tmp/novaplugins-kilo.iso')
|
||||
'/tmp/%s' % XS_PLUGIN_ISO, prompt='Y\n')
|
||||
ssh(himn, username, password, 'rm', '/tmp/%s' % XS_PLUGIN_ISO)
|
||||
|
||||
|
||||
def forward_from_himn(eth):
|
||||
|
@ -301,6 +301,78 @@ def install_logrotate_script(himn, username, password):
|
|||
CRONTAB''')
|
||||
|
||||
|
||||
def modify_neutron_rootwrap_conf(himn, username, password):
|
||||
"""Set xenapi configurations"""
|
||||
filename = '/etc/neutron/rootwrap.conf'
|
||||
cf = ConfigParser.ConfigParser()
|
||||
try:
|
||||
cf.read(filename)
|
||||
cf.set('xenapi', 'xenapi_connection_url', 'http://%s' % himn)
|
||||
cf.set('xenapi', 'xenapi_connection_username', username)
|
||||
cf.set('xenapi', 'xenapi_connection_password', password)
|
||||
cf.write(open(filename, 'w'))
|
||||
except Exception:
|
||||
reportError("Fail to modify file %s", filename)
|
||||
logging.info('Modify file %s successfully', filename)
|
||||
|
||||
|
||||
def modify_neutron_ovs_agent_conf(int_br, br_mappings):
|
||||
filename = '/etc/neutron/plugins/ml2/ml2_conf.ini'
|
||||
cf = ConfigParser.ConfigParser()
|
||||
try:
|
||||
cf.read(filename)
|
||||
cf.set('agent', 'root_helper',
|
||||
'neutron-rootwrap-xen-dom0 /etc/neutron/rootwrap.conf')
|
||||
cf.set('agent', 'root_helper_daemon', '')
|
||||
cf.set('agent', 'minimize_polling', False)
|
||||
cf.set('ovs', 'integration_bridge', int_br)
|
||||
cf.set('ovs', 'bridge_mappings', br_mappings)
|
||||
cf.write(open(filename, 'w'))
|
||||
except Exception:
|
||||
reportError("Fail to modify %s", filename)
|
||||
logging.info('Modify %s successfully', filename)
|
||||
|
||||
|
||||
def get_private_network_ethX():
|
||||
# find out bridge which is used for private network
|
||||
values = astute['network_scheme']['transformations']
|
||||
for item in values:
|
||||
if item['action'] == 'add-port' and item['bridge'] == 'br-aux':
|
||||
return item['name']
|
||||
|
||||
|
||||
def find_bridge_mappings(astute, himn, username, password):
|
||||
ethX = get_private_network_ethX()
|
||||
if not ethX:
|
||||
reportError("Cannot find eth used for private network")
|
||||
|
||||
# find the ethX mac in /sys/class/net/ethX/address
|
||||
fo = open('/sys/class/net/%s/address' % ethX, 'r')
|
||||
mac = fo.readline()
|
||||
fo.close()
|
||||
network_uuid = ssh(himn, username, password,
|
||||
'xe vif-list params=network-uuid minimal=true MAC=%s' % mac)
|
||||
bridge = ssh(himn, username, password,
|
||||
'xe network-param-get param-name=bridge uuid=%s' % network_uuid)
|
||||
|
||||
# find physical network name
|
||||
phynet_setting = astute['quantum_settings']['L2']['phys_nets']
|
||||
physnet = phynet_setting.keys()[0]
|
||||
return physnet + ':' + bridge
|
||||
|
||||
|
||||
def restart_services(service_name):
|
||||
execute('stop', service_name)
|
||||
execute('start', service_name)
|
||||
|
||||
|
||||
def enable_linux_bridge(himn, username, password):
|
||||
# When using OVS under XS6.5, it will prevent use of Linux bridge in
|
||||
# Dom0, but neutron-openvswitch-agent in compute node will use Linux
|
||||
# bridge, so we remove this restriction here
|
||||
ssh(himn, username, password, 'rm -f /etc/modprobe.d/blacklist-bridge')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
install_xenapi_sdk()
|
||||
astute = get_astute(ASTUTE_PATH)
|
||||
|
@ -318,12 +390,20 @@ if __name__ == '__main__':
|
|||
endpoints, HIMN_IP, himn_local, username, password)
|
||||
if install_xapi:
|
||||
install_suppack(HIMN_IP, username, password)
|
||||
enable_linux_bridge(HIMN_IP, username, password)
|
||||
forward_from_himn(himn_eth)
|
||||
|
||||
# port forwarding for novnc
|
||||
forward_port('br-mgmt', himn_eth, HIMN_IP, '80')
|
||||
|
||||
create_novacompute_conf(HIMN_IP, username, password, public_ip)
|
||||
restart_nova_services()
|
||||
restart_services('nova-compute')
|
||||
|
||||
install_logrotate_script(HIMN_IP, username, password)
|
||||
|
||||
# neutron-l2-agent in compute node
|
||||
modify_neutron_rootwrap_conf(HIMN_IP, username, password)
|
||||
br_mappings = find_bridge_mappings(astute, HIMN_IP,
|
||||
username, password)
|
||||
modify_neutron_ovs_agent_conf(INT_BRIDGE, br_mappings)
|
||||
restart_services('neutron-plugin-openvswitch-agent')
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# build-xenserver-suppack.sh
|
||||
|
||||
This script is used to build iso for XenServer Dom0 xapi plugin.
|
||||
|
||||
It will build both Nova and Neutron Dom0 plugin RPM packages firstly,
|
||||
and then make them in one ISO.
|
||||
|
||||
|
||||
## usage:
|
||||
|
||||
#####./build-xenserver-suppack.sh $xs-version $xs-build $os-git-branch $os-plugin-version
|
||||
|
||||
* xs-version: XenServer version which can be used for this plugin
|
||||
|
||||
* xs-build: XenServer build number
|
||||
|
||||
* os-git-branch: OpenStack branch that's used for building this plugin
|
||||
|
||||
* os-plugin-version: OpenStack XenServer Dom0 plguin version
|
||||
|
||||
|
||||
|
||||
*NOTE: If no input parameters given, default values are used*
|
||||
|
||||
*xs-version: 6.5*
|
||||
|
||||
*xs-build: 90233c*
|
||||
|
||||
*os-git-branch: stable/liberty*
|
||||
|
||||
*os-plugin-version: 2015.1*
|
||||
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -eux
|
||||
|
||||
# =============================================
|
||||
# Usage of this script:
|
||||
# ./build-xenserver-suppack.sh xs-version xs-build git-branch plugin-version
|
||||
# or
|
||||
# ./build-xenserver-suppack.sh
|
||||
#
|
||||
# You can provide explict input parameters or you can use the default ones:
|
||||
# XenServer version
|
||||
# XenServer build
|
||||
# OpenStack release branch
|
||||
# XenServer OpenStack plugin version
|
||||
|
||||
|
||||
THIS_FILE=$(readlink -f $0)
|
||||
FUELPLUG_UTILS_ROOT=$(dirname $THIS_FILE)
|
||||
DEPLOYMENT_SCRIPT_ROOT=$(dirname $FUELPLUG_UTILS_ROOT)
|
||||
cd $FUELPLUG_UTILS_ROOT
|
||||
rm -rf xenserver-suppack
|
||||
mkdir -p xenserver-suppack && cd xenserver-suppack
|
||||
|
||||
|
||||
# =============================================
|
||||
# Configurable items
|
||||
|
||||
# xenserver version info
|
||||
XS_VERSION=${1:-"6.5"}
|
||||
XS_BUILD=${2:-"90233c"}
|
||||
|
||||
# branch info
|
||||
GITBRANCH=${3:-"stable/liberty"}
|
||||
|
||||
# nova and neutron xenserver dom0 plugin version
|
||||
XS_PLUGIN_VERSION=${4:-"2015.1"}
|
||||
|
||||
# OpenStack release
|
||||
OS_RELEASE=liberty
|
||||
|
||||
# repository info
|
||||
NOVA_GITREPO="https://git.openstack.org/openstack/nova"
|
||||
NEUTRON_GITREPO="https://git.openstack.org/openstack/neutron"
|
||||
DDK_ROOT_URL="http://copper.eng.hq.xensource.com/builds/ddk-xs6_2.tgz"
|
||||
RPM_BUILDER_REPO="https://github.com/citrix-openstack/xenserver-nova-suppack-builder"
|
||||
|
||||
# Update system and install dependencies
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
# =============================================
|
||||
# Check out rpm packaging repo
|
||||
rm -rf xenserver-nova-suppack-builder
|
||||
git clone $RPM_BUILDER_REPO
|
||||
|
||||
|
||||
# =============================================
|
||||
# Create nova rpm file
|
||||
rm -rf nova
|
||||
git clone "$NOVA_GITREPO" nova
|
||||
cd nova
|
||||
git fetch origin "$GITBRANCH"
|
||||
git checkout FETCH_HEAD
|
||||
# patch xenhost as this file is not merged to liberty
|
||||
cp $DEPLOYMENT_SCRIPT_ROOT/patchset/xenhost plugins/xenserver/xenapi/etc/xapi.d/plugins/
|
||||
cd ..
|
||||
|
||||
cp -r xenserver-nova-suppack-builder/plugins/xenserver/xenapi/* nova/plugins/xenserver/xenapi/
|
||||
cd nova/plugins/xenserver/xenapi/contrib
|
||||
./build-rpm.sh $XS_PLUGIN_VERSION
|
||||
cd $FUELPLUG_UTILS_ROOT/xenserver-suppack/
|
||||
RPMFILE=$(find -name "openstack-xen-plugins-*.noarch.rpm" -print)
|
||||
|
||||
|
||||
# =============================================
|
||||
# Create neutron rpm file
|
||||
rm -rf neutron
|
||||
git clone "$NEUTRON_GITREPO" neutron
|
||||
cd neutron
|
||||
git fetch origin "$GITBRANCH"
|
||||
git checkout FETCH_HEAD
|
||||
# patch netwrap as this file is not merged to liberty
|
||||
cp $DEPLOYMENT_SCRIPT_ROOT/patchset/netwrap \
|
||||
neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/
|
||||
chmod +x neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/netwrap
|
||||
cd ..
|
||||
|
||||
cp -r xenserver-nova-suppack-builder/neutron/* \
|
||||
neutron/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/
|
||||
cd neutron/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/contrib
|
||||
./build-rpm.sh $XS_PLUGIN_VERSION
|
||||
cd $FUELPLUG_UTILS_ROOT/xenserver-suppack/
|
||||
NEUTRON_RPMFILE=$(find -name "openstack-neutron-xen-plugins-*.noarch.rpm" -print)
|
||||
|
||||
|
||||
# =============================================
|
||||
# Create Supplemental pack
|
||||
rm -rf suppack
|
||||
mkdir suppack
|
||||
|
||||
DDKROOT=$(mktemp -d)
|
||||
|
||||
wget -qO - "$DDK_ROOT_URL" | sudo tar -xzf - -C "$DDKROOT"
|
||||
|
||||
sudo mkdir $DDKROOT/mnt/host
|
||||
sudo mount --bind $(pwd) $DDKROOT/mnt/host
|
||||
|
||||
sudo tee $DDKROOT/buildscript.py << EOF
|
||||
from xcp.supplementalpack import *
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser()
|
||||
parser.add_option('--pdn', dest="product_name")
|
||||
parser.add_option('--pdv', dest="product_version")
|
||||
parser.add_option('--bld', dest="build")
|
||||
parser.add_option('--out', dest="outdir")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
xs = Requires(originator='xs', name='main', test='ge',
|
||||
product='XenServer', version='$XS_VERSION',
|
||||
build='$XS_BUILD')
|
||||
|
||||
setup(originator='xs', name='xenserverplugins-$OS_RELEASE', product='XenServer',
|
||||
version=options.product_version, build=options.build, vendor='Citrix Systems, Inc.',
|
||||
description="OpenStack XenServer Plugins", packages=args, requires=[xs],
|
||||
outdir=options.outdir, output=['iso'])
|
||||
EOF
|
||||
|
||||
sudo chroot $DDKROOT python buildscript.py \
|
||||
--pdn=xenserverplugins \
|
||||
--pdv=$OS_RELEASE \
|
||||
--bld=0 \
|
||||
--out=/mnt/host/suppack \
|
||||
/mnt/host/$RPMFILE \
|
||||
/mnt/host/$NEUTRON_RPMFILE
|
||||
|
||||
# Cleanup
|
||||
sudo umount $DDKROOT/mnt/host
|
||||
sudo rm -rf "$DDKROOT"
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
#
|
||||
# XenAPI plugin for executing network commands (ovs, iptables, etc) on dom0
|
||||
# Changes in this file merged post liberty, CommitID:
|
||||
# b0cef88866db3d325974b1691ac3e1030144ee19
|
||||
#
|
||||
|
||||
import gettext
|
||||
gettext.install('neutron', unicode=1)
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import subprocess
|
||||
|
||||
import XenAPIPlugin
|
||||
|
||||
|
||||
ALLOWED_CMDS = [
|
||||
'ip',
|
||||
'ipset',
|
||||
'iptables-save',
|
||||
'iptables-restore',
|
||||
'ip6tables-save',
|
||||
'ip6tables-restore',
|
||||
'sysctl',
|
||||
# NOTE(yamamoto): of_interface=native doesn't use ovs-ofctl
|
||||
'ovs-ofctl',
|
||||
'ovs-vsctl',
|
||||
'ovsdb-client',
|
||||
]
|
||||
|
||||
|
||||
class PluginError(Exception):
|
||||
"""Base Exception class for all plugin errors."""
|
||||
def __init__(self, *args):
|
||||
Exception.__init__(self, *args)
|
||||
|
||||
def _run_command(cmd, cmd_input):
|
||||
"""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
|
||||
proc = subprocess.Popen(cmd, shell=False, stdin=pipe, stdout=pipe,
|
||||
stderr=pipe, close_fds=True)
|
||||
(out, err) = proc.communicate(cmd_input)
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise PluginError(err)
|
||||
return out
|
||||
|
||||
|
||||
def run_command(session, args):
|
||||
cmd = json.loads(args.get('cmd'))
|
||||
if cmd and cmd[0] not in ALLOWED_CMDS:
|
||||
msg = _("Dom0 execution of '%s' is not permitted") % cmd[0]
|
||||
raise PluginError(msg)
|
||||
result = _run_command(cmd, json.loads(args.get('cmd_input', 'null')))
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
XenAPIPlugin.dispatch({"run_command": run_command})
|
|
@ -0,0 +1,482 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2011 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# NOTE: XenServer still only supports Python 2.4 in it's dom0 userspace
|
||||
# which means the Nova xenapi plugins must use only Python 2.4 features
|
||||
|
||||
#
|
||||
# XenAPI plugin for host operations
|
||||
# Changes in this file will be merged post liberty
|
||||
# OVS interim bridge: https://review.openstack.org/#/c/242846/29
|
||||
# Neutron security group: https://review.openstack.org/#/c/251271/10
|
||||
#
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import utils
|
||||
|
||||
import pluginlib_nova as pluginlib
|
||||
import XenAPI
|
||||
import XenAPIPlugin
|
||||
|
||||
try:
|
||||
import xmlrpclib
|
||||
except ImportError:
|
||||
import six.moves.xmlrpc_client as xmlrpclib
|
||||
|
||||
|
||||
pluginlib.configure_logging("xenhost")
|
||||
_ = pluginlib._
|
||||
|
||||
|
||||
host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)")
|
||||
config_file_path = "/usr/etc/xenhost.conf"
|
||||
DEFAULT_TRIES = 23
|
||||
DEFAULT_SLEEP = 10
|
||||
|
||||
|
||||
def jsonify(fnc):
|
||||
def wrapper(*args, **kwargs):
|
||||
return json.dumps(fnc(*args, **kwargs))
|
||||
return wrapper
|
||||
|
||||
|
||||
class TimeoutError(StandardError):
|
||||
pass
|
||||
|
||||
|
||||
def _run_command(cmd, cmd_input=None):
|
||||
"""Wrap utils.run_command to raise PluginError on failure
|
||||
"""
|
||||
try:
|
||||
return utils.run_command(cmd, cmd_input=cmd_input)
|
||||
except utils.SubprocessException, e:
|
||||
raise pluginlib.PluginError(e.err)
|
||||
|
||||
|
||||
def _resume_compute(session, compute_ref, compute_uuid):
|
||||
"""Resume compute node on slave host after pool join. This has to
|
||||
happen regardless of the success or failure of the join operation."""
|
||||
try:
|
||||
# session is valid if the join operation has failed
|
||||
session.xenapi.VM.start(compute_ref, False, True)
|
||||
except XenAPI.Failure, e:
|
||||
# if session is invalid, e.g. xapi has restarted, then the pool
|
||||
# join has been successful, wait for xapi to become alive again
|
||||
for c in xrange(0, DEFAULT_TRIES):
|
||||
try:
|
||||
_run_command(["xe", "vm-start", "uuid=%s" % compute_uuid])
|
||||
return
|
||||
except pluginlib.PluginError, e:
|
||||
logging.exception('Waited %d seconds for the slave to '
|
||||
'become available.' % (c * DEFAULT_SLEEP))
|
||||
time.sleep(DEFAULT_SLEEP)
|
||||
raise pluginlib.PluginError('Unrecoverable error: the host has '
|
||||
'not come back for more than %d seconds'
|
||||
% (DEFAULT_SLEEP * (DEFAULT_TRIES + 1)))
|
||||
|
||||
|
||||
@jsonify
|
||||
def set_host_enabled(self, arg_dict):
|
||||
"""Sets this host's ability to accept new instances.
|
||||
It will otherwise continue to operate normally.
|
||||
"""
|
||||
enabled = arg_dict.get("enabled")
|
||||
if enabled is None:
|
||||
raise pluginlib.PluginError(
|
||||
_("Missing 'enabled' argument to set_host_enabled"))
|
||||
|
||||
host_uuid = arg_dict['host_uuid']
|
||||
if enabled == "true":
|
||||
result = _run_command(["xe", "host-enable", "uuid=%s" % host_uuid])
|
||||
elif enabled == "false":
|
||||
result = _run_command(["xe", "host-disable", "uuid=%s" % host_uuid])
|
||||
else:
|
||||
raise pluginlib.PluginError(_("Illegal enabled status: %s") % enabled)
|
||||
# Should be empty string
|
||||
if result:
|
||||
raise pluginlib.PluginError(result)
|
||||
# Return the current enabled status
|
||||
cmd = ["xe", "host-param-get", "uuid=%s" % host_uuid, "param-name=enabled"]
|
||||
host_enabled = _run_command(cmd)
|
||||
if host_enabled == "true":
|
||||
status = "enabled"
|
||||
else:
|
||||
status = "disabled"
|
||||
return {"status": status}
|
||||
|
||||
|
||||
def _write_config_dict(dct):
|
||||
conf_file = file(config_file_path, "w")
|
||||
json.dump(dct, conf_file)
|
||||
conf_file.close()
|
||||
|
||||
|
||||
def _get_config_dict():
|
||||
"""Returns a dict containing the key/values in the config file.
|
||||
If the file doesn't exist, it is created, and an empty dict
|
||||
is returned.
|
||||
"""
|
||||
try:
|
||||
conf_file = file(config_file_path)
|
||||
config_dct = json.load(conf_file)
|
||||
conf_file.close()
|
||||
except IOError:
|
||||
# File doesn't exist
|
||||
config_dct = {}
|
||||
# Create the file
|
||||
_write_config_dict(config_dct)
|
||||
return config_dct
|
||||
|
||||
|
||||
@jsonify
|
||||
def get_config(self, arg_dict):
|
||||
"""Return the value stored for the specified key, or None if no match."""
|
||||
conf = _get_config_dict()
|
||||
params = arg_dict["params"]
|
||||
try:
|
||||
dct = json.loads(params)
|
||||
except Exception, e:
|
||||
dct = params
|
||||
key = dct["key"]
|
||||
ret = conf.get(key)
|
||||
if ret is None:
|
||||
# Can't jsonify None
|
||||
return "None"
|
||||
return ret
|
||||
|
||||
|
||||
@jsonify
|
||||
def set_config(self, arg_dict):
|
||||
"""Write the specified key/value pair, overwriting any existing value."""
|
||||
conf = _get_config_dict()
|
||||
params = arg_dict["params"]
|
||||
try:
|
||||
dct = json.loads(params)
|
||||
except Exception, e:
|
||||
dct = params
|
||||
key = dct["key"]
|
||||
val = dct["value"]
|
||||
if val is None:
|
||||
# Delete the key, if present
|
||||
conf.pop(key, None)
|
||||
else:
|
||||
conf.update({key: val})
|
||||
_write_config_dict(conf)
|
||||
|
||||
|
||||
def iptables_config(session, args):
|
||||
# command should be either save or restore
|
||||
logging.debug("iptables_config:enter")
|
||||
logging.debug("iptables_config: args=%s", args)
|
||||
cmd_args = pluginlib.exists(args, 'cmd_args')
|
||||
logging.debug("iptables_config: cmd_args=%s", cmd_args)
|
||||
process_input = pluginlib.optional(args, 'process_input')
|
||||
logging.debug("iptables_config: process_input=%s", process_input)
|
||||
cmd = json.loads(cmd_args)
|
||||
cmd = map(str, cmd)
|
||||
|
||||
# either execute iptable-save or iptables-restore
|
||||
# command must be only one of these two
|
||||
# process_input must be used only with iptables-restore
|
||||
if len(cmd) > 0 and cmd[0] in ('iptables-save',
|
||||
'iptables-restore',
|
||||
'ip6tables-save',
|
||||
'ip6tables-restore'):
|
||||
result = _run_command(cmd, process_input)
|
||||
ret_str = json.dumps(dict(out=result,
|
||||
err=''))
|
||||
logging.debug("iptables_config:exit")
|
||||
return ret_str
|
||||
else:
|
||||
# else don't do anything and return an error
|
||||
raise pluginlib.PluginError(_("Invalid iptables command"))
|
||||
|
||||
|
||||
def network_config(session, args):
|
||||
# function to config OVS bridge and Linux bridge
|
||||
ALLOWED_CMDS = [
|
||||
'ovs-vsctl',
|
||||
'brctl',
|
||||
'ip'
|
||||
]
|
||||
cmd = json.loads(args.get('cmd'))
|
||||
if cmd is None or cmd == []:
|
||||
msg = _("empty command is supplied")
|
||||
raise pluginlib.PluginError(msg)
|
||||
if cmd[0] not in ALLOWED_CMDS:
|
||||
msg = _("Dom0 execution of '%s' is not permitted") % cmd[0]
|
||||
raise pluginlib.PluginError(msg)
|
||||
result = _run_command(cmd, json.loads(args.get('cmd_input', 'null')))
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
def _power_action(action, arg_dict):
|
||||
# Host must be disabled first
|
||||
host_uuid = arg_dict['host_uuid']
|
||||
result = _run_command(["xe", "host-disable", "uuid=%s" % host_uuid])
|
||||
if result:
|
||||
raise pluginlib.PluginError(result)
|
||||
# All running VMs must be shutdown
|
||||
result = _run_command(["xe", "vm-shutdown", "--multiple",
|
||||
"resident-on=%s" % host_uuid])
|
||||
if result:
|
||||
raise pluginlib.PluginError(result)
|
||||
cmds = {"reboot": "host-reboot",
|
||||
"startup": "host-power-on",
|
||||
"shutdown": "host-shutdown",}
|
||||
result = _run_command(["xe", cmds[action], "uuid=%s" % host_uuid])
|
||||
# Should be empty string
|
||||
if result:
|
||||
raise pluginlib.PluginError(result)
|
||||
return {"power_action": action}
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_reboot(self, arg_dict):
|
||||
"""Reboots the host."""
|
||||
return _power_action("reboot", arg_dict)
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_shutdown(self, arg_dict):
|
||||
"""Reboots the host."""
|
||||
return _power_action("shutdown", arg_dict)
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_start(self, arg_dict):
|
||||
"""Starts the host. Currently not feasible, since the host
|
||||
runs on the same machine as Xen.
|
||||
"""
|
||||
return _power_action("startup", arg_dict)
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_join(self, arg_dict):
|
||||
"""Join a remote host into a pool whose master is the host
|
||||
where the plugin is called from. The following constraints apply:
|
||||
|
||||
- The host must have no VMs running, except nova-compute, which will be
|
||||
shut down (and restarted upon pool-join) automatically,
|
||||
- The host must have no shared storage currently set up,
|
||||
- The host must have the same license of the master,
|
||||
- The host must have the same supplemental packs as the master."""
|
||||
session = XenAPI.Session(arg_dict.get("url"))
|
||||
session.login_with_password(arg_dict.get("user"),
|
||||
arg_dict.get("password"))
|
||||
compute_ref = session.xenapi.VM.get_by_uuid(arg_dict.get('compute_uuid'))
|
||||
session.xenapi.VM.clean_shutdown(compute_ref)
|
||||
try:
|
||||
if arg_dict.get("force"):
|
||||
session.xenapi.pool.join(arg_dict.get("master_addr"),
|
||||
arg_dict.get("master_user"),
|
||||
arg_dict.get("master_pass"))
|
||||
else:
|
||||
session.xenapi.pool.join_force(arg_dict.get("master_addr"),
|
||||
arg_dict.get("master_user"),
|
||||
arg_dict.get("master_pass"))
|
||||
finally:
|
||||
_resume_compute(session, compute_ref, arg_dict.get("compute_uuid"))
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_data(self, arg_dict):
|
||||
"""Runs the commands on the xenstore host to return the current status
|
||||
information.
|
||||
"""
|
||||
host_uuid = arg_dict['host_uuid']
|
||||
resp = _run_command(["xe", "host-param-list", "uuid=%s" % host_uuid])
|
||||
parsed_data = parse_response(resp)
|
||||
# We have the raw dict of values. Extract those that we need,
|
||||
# and convert the data types as needed.
|
||||
ret_dict = cleanup(parsed_data)
|
||||
# Add any config settings
|
||||
config = _get_config_dict()
|
||||
ret_dict.update(config)
|
||||
return ret_dict
|
||||
|
||||
|
||||
def parse_response(resp):
|
||||
data = {}
|
||||
for ln in resp.splitlines():
|
||||
if not ln:
|
||||
continue
|
||||
mtch = host_data_pattern.match(ln.strip())
|
||||
try:
|
||||
k, v = mtch.groups()
|
||||
data[k] = v
|
||||
except AttributeError:
|
||||
# Not a valid line; skip it
|
||||
continue
|
||||
return data
|
||||
|
||||
|
||||
@jsonify
|
||||
def host_uptime(self, arg_dict):
|
||||
"""Returns the result of the uptime command on the xenhost."""
|
||||
return {"uptime": _run_command(['uptime'])}
|
||||
|
||||
|
||||
def cleanup(dct):
|
||||
"""Take the raw KV pairs returned and translate them into the
|
||||
appropriate types, discarding any we don't need.
|
||||
"""
|
||||
def safe_int(val):
|
||||
"""Integer values will either be string versions of numbers,
|
||||
or empty strings. Convert the latter to nulls.
|
||||
"""
|
||||
try:
|
||||
return int(val)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def strip_kv(ln):
|
||||
return [val.strip() for val in ln.split(":", 1)]
|
||||
|
||||
out = {}
|
||||
|
||||
# sbs = dct.get("supported-bootloaders", "")
|
||||
# out["host_supported-bootloaders"] = sbs.split("; ")
|
||||
# out["host_suspend-image-sr-uuid"] = dct.get("suspend-image-sr-uuid", "")
|
||||
# out["host_crash-dump-sr-uuid"] = dct.get("crash-dump-sr-uuid", "")
|
||||
# out["host_local-cache-sr"] = dct.get("local-cache-sr", "")
|
||||
out["enabled"] = dct.get("enabled", "true") == "true"
|
||||
out["host_memory"] = omm = {}
|
||||
omm["total"] = safe_int(dct.get("memory-total", ""))
|
||||
omm["overhead"] = safe_int(dct.get("memory-overhead", ""))
|
||||
omm["free"] = safe_int(dct.get("memory-free", ""))
|
||||
omm["free-computed"] = safe_int(
|
||||
dct.get("memory-free-computed", ""))
|
||||
|
||||
# out["host_API-version"] = avv = {}
|
||||
# avv["vendor"] = dct.get("API-version-vendor", "")
|
||||
# avv["major"] = safe_int(dct.get("API-version-major", ""))
|
||||
# avv["minor"] = safe_int(dct.get("API-version-minor", ""))
|
||||
|
||||
out["enabled"] = dct.get("enabled", True)
|
||||
out["host_uuid"] = dct.get("uuid", None)
|
||||
out["host_name-label"] = dct.get("name-label", "")
|
||||
out["host_name-description"] = dct.get("name-description", "")
|
||||
# out["host_host-metrics-live"] = dct.get(
|
||||
# "host-metrics-live", "false") == "true"
|
||||
out["host_hostname"] = dct.get("hostname", "")
|
||||
out["host_ip_address"] = dct.get("address", "")
|
||||
oc = dct.get("other-config", "")
|
||||
out["host_other-config"] = ocd = {}
|
||||
if oc:
|
||||
for oc_fld in oc.split("; "):
|
||||
ock, ocv = strip_kv(oc_fld)
|
||||
ocd[ock] = ocv
|
||||
|
||||
capabilities = dct.get("capabilities", "")
|
||||
out["host_capabilities"] = capabilities.replace(";", "").split()
|
||||
# out["host_allowed-operations"] = dct.get(
|
||||
# "allowed-operations", "").split("; ")
|
||||
# lsrv = dct.get("license-server", "")
|
||||
# out["host_license-server"] = ols = {}
|
||||
# if lsrv:
|
||||
# for lspart in lsrv.split("; "):
|
||||
# lsk, lsv = lspart.split(": ")
|
||||
# if lsk == "port":
|
||||
# ols[lsk] = safe_int(lsv)
|
||||
# else:
|
||||
# ols[lsk] = lsv
|
||||
# sv = dct.get("software-version", "")
|
||||
# out["host_software-version"] = osv = {}
|
||||
# if sv:
|
||||
# for svln in sv.split("; "):
|
||||
# svk, svv = strip_kv(svln)
|
||||
# osv[svk] = svv
|
||||
cpuinf = dct.get("cpu_info", "")
|
||||
out["host_cpu_info"] = ocp = {}
|
||||
if cpuinf:
|
||||
for cpln in cpuinf.split("; "):
|
||||
cpk, cpv = strip_kv(cpln)
|
||||
if cpk in ("cpu_count", "family", "model", "stepping"):
|
||||
ocp[cpk] = safe_int(cpv)
|
||||
else:
|
||||
ocp[cpk] = cpv
|
||||
# out["host_edition"] = dct.get("edition", "")
|
||||
# out["host_external-auth-service-name"] = dct.get(
|
||||
# "external-auth-service-name", "")
|
||||
return out
|
||||
|
||||
def query_gc(session, sr_uuid, vdi_uuid):
|
||||
result = _run_command(["/opt/xensource/sm/cleanup.py",
|
||||
"-q", "-u", sr_uuid])
|
||||
# Example output: "Currently running: True"
|
||||
return result[19:].strip() == "True"
|
||||
|
||||
def get_pci_device_details(session):
|
||||
"""Returns a string that is a list of pci devices with details.
|
||||
|
||||
This string is obtained by running the command lspci. With -vmm option,
|
||||
it dumps PCI device data in machine readable form. This verbose format
|
||||
display a sequence of records separated by a blank line. We will also
|
||||
use option "-n" to get vendor_id and device_id as numeric values and
|
||||
the "-k" option to get the kernel driver used if any.
|
||||
"""
|
||||
return _run_command(["lspci", "-vmmnk"])
|
||||
|
||||
|
||||
def get_pci_type(session, pci_device):
|
||||
"""Returns the type of the PCI device (type-PCI, type-VF or type-PF).
|
||||
|
||||
pci-device -- The address of the pci device
|
||||
"""
|
||||
# We need to add the domain if it is missing
|
||||
if pci_device.count(':') == 1:
|
||||
pci_device = "0000:" + pci_device
|
||||
output = _run_command(["ls", "/sys/bus/pci/devices/" + pci_device + "/"])
|
||||
|
||||
if "physfn" in output:
|
||||
return "type-VF"
|
||||
if "virtfn" in output:
|
||||
return "type-PF"
|
||||
return "type-PCI"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Support both serialized and non-serialized plugin approaches
|
||||
_, methodname = xmlrpclib.loads(sys.argv[1])
|
||||
if methodname in ['query_gc', 'get_pci_device_details', 'get_pci_type']:
|
||||
utils.register_plugin_calls(query_gc,
|
||||
get_pci_device_details,
|
||||
get_pci_type)
|
||||
|
||||
XenAPIPlugin.dispatch(
|
||||
{"host_data": host_data,
|
||||
"set_host_enabled": set_host_enabled,
|
||||
"host_shutdown": host_shutdown,
|
||||
"host_reboot": host_reboot,
|
||||
"host_start": host_start,
|
||||
"host_join": host_join,
|
||||
"get_config": get_config,
|
||||
"set_config": set_config,
|
||||
"iptables_config": iptables_config,
|
||||
"network_config": network_config,
|
||||
"host_uptime": host_uptime})
|
Binary file not shown.
Loading…
Reference in New Issue