Neutron VPNaaS plugin for Fuel

VPNaaS (VPN-as-a-Service) is a Neutron extension that introduces VPN features.
This plugin supports both HA and multinode modes.
Co-authored with Andrey Epifanov (aepifanov@mirantis.com)
This commit is contained in:
Sergey Kolekonov 2014-11-26 13:51:57 +03:00
commit 59d8b45a71
31 changed files with 2009 additions and 0 deletions

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
VPNaaS plugin
============
VPNaaS (VPN-as-a-Service) is a Neutron extension that introduces VPN feature set.

View File

@ -0,0 +1,2 @@
if $cluster_mode == 'ha_compact' { include vpnaas::ha }
else { include vpnaas }

View File

@ -0,0 +1,7 @@
source 'https://rubygems.org'
puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 3.3']
gem 'puppet', puppetversion
gem 'puppetlabs_spec_helper', '>= 0.1.0'
gem 'puppet-lint', '>= 0.3.2'
gem 'facter', '>= 1.7.0'

View File

@ -0,0 +1,48 @@
# VPNaaS
#### Table of Contents
1. [Overview](#overview)
2. [Module Description - What the module does and why it is useful](#module-description)
3. [Setup - The basics of getting started with VPNaaA](#setup)
* [What VPNaaS affects](#what-vpnaas-affects)
* [Beginning with VPNaaS](#beginning-with-vpnaas)
4. [Reference - An under-the-hood peek at what the module is doing and how](#reference)
5. [Limitations - OS compatibility, etc.](#limitations)
## Overview
This is VPNaaS plugin for FUEL, which provides an ability to setup VPNaaS Neuton extention
that introduces VPN feature set.
## Module Description
VPNaaS Neutron extention provides additional functionality for the building VPN
connections based on opensource for IPsec based VPNs using just static routing.
This functionality might be used for setup VPN connetion between two tenats which
are placed in the diffent Openstack environments, for example, between Public and
Private Clouds.
## Setup
### What VPNaaS affects
* This plugin contains OpenSwan package for Ubuntu and CentOS.
* During installation manifests make some modification in Horizon for enable VPNaaS functionality.
* Also this plugin replaces l3-agent on vpn-agent, which completely based on l3-agent and has additional VPN functionality.
### Beginning with VPNaaS
How to use VPNaaS you can find here:
https://www.mirantis.com/blog/mirantis-openstack-express-vpn-service-vpnaas-step-step/
## Reference
Here, list the classes, types, providers, facts, etc contained in your module.
This section should include all of the under-the-hood workings of your module so
people know what the module is touching on their system but don't need to mess
with things. (We are working on automating this section!)
## Limitations
This plugin supports only the following OS: CentOS 6.4 and Ubuntu 12.04.

View File

@ -0,0 +1,18 @@
require 'rubygems'
require 'puppetlabs_spec_helper/rake_tasks'
require 'puppet-lint/tasks/puppet-lint'
PuppetLint.configuration.send('disable_80chars')
PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"]
desc "Validate manifests, templates, and ruby files"
task :validate do
Dir['manifests/**/*.pp'].each do |manifest|
sh "puppet parser validate --noop #{manifest}"
end
Dir['spec/**/*.rb','lib/**/*.rb'].each do |ruby_file|
sh "ruby -c #{ruby_file}" unless ruby_file =~ /spec\/fixtures/
end
Dir['templates/**/*.erb'].each do |template|
sh "erb -P -x -T '-' #{template} | ruby -c"
end
end

View File

@ -0,0 +1,594 @@
#!/bin/bash
#
#
# OpenStack L3 Service (neutron-vpn-agent)
#
# Description: Manages an OpenStack L3 Service (neutron-vpn-agent) process as an HA resource
#
# Authors: Emilien Macchi
# Mainly inspired by the Nova Network resource agent written by Emilien Macchi & Sebastien Han
#
# Support: openstack@lists.launchpad.net
# License: Apache Software License (ASL) 2.0
#
#
# See usage() function below for more details ...
#
# OCF instance parameters:
# OCF_RESKEY_binary
# OCF_RESKEY_config
# OCF_RESKEY_plugin_config
# OCF_RESKEY_vpn_config
# OCF_RESKEY_user
# OCF_RESKEY_pid
# OCF_RESKEY_neutron_server_port
# OCF_RESKEY_additional_parameters
#######################################################################
# Initialization:
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
umask 0022
#######################################################################
# Fill in some defaults if no values are specified
PATH=/sbin:/usr/sbin:/bin:/usr/bin
OCF_RESKEY_binary_default="neutron-vpn-agent"
OCF_RESKEY_config_default="/etc/neutron/neutron.conf"
OCF_RESKEY_keystone_config_default="/etc/keystone/keystone.conf"
OCF_RESKEY_vpn_config_default="/etc/neutron/vpn_agent.ini"
OCF_RESKEY_plugin_config_default="/etc/neutron/l3_agent.ini"
OCF_RESKEY_log_file_default="/var/log/neutron/vpn-agent.log"
OCF_RESKEY_user_default="neutron"
OCF_RESKEY_pid_default="${HA_RSCTMP}/${__SCRIPT_NAME}/${__SCRIPT_NAME}.pid"
OCF_RESKEY_os_auth_url_default="http://localhost:5000/v2.0"
OCF_RESKEY_username_default="neutron"
OCF_RESKEY_password_default="neutron_pass"
OCF_RESKEY_tenant_default="services"
OCF_RESKEY_external_bridge_default="br-ex"
OCF_RESKEY_multiple_agents_default=true
OCF_RESKEY_rescheduling_tries_default=5
OCF_RESKEY_rescheduling_interval_default=33
OCF_RESKEY_debug_default=false
: ${OCF_RESKEY_os_auth_url=${OCF_RESKEY_os_auth_url_default}}
: ${OCF_RESKEY_username=${OCF_RESKEY_username_default}}
: ${OCF_RESKEY_password=${OCF_RESKEY_password_default}}
: ${OCF_RESKEY_tenant=${OCF_RESKEY_tenant_default}}
: ${OCF_RESKEY_binary=${OCF_RESKEY_binary_default}}
: ${OCF_RESKEY_config=${OCF_RESKEY_config_default}}
: ${OCF_RESKEY_keystone_config=${OCF_RESKEY_keystone_config_default}}
: ${OCF_RESKEY_plugin_config=${OCF_RESKEY_plugin_config_default}}
: ${OCF_RESKEY_vpn_config=${OCF_RESKEY_vpn_config_default}}
: ${OCF_RESKEY_user=${OCF_RESKEY_user_default}}
: ${OCF_RESKEY_pid=${OCF_RESKEY_pid_default}}
: ${OCF_RESKEY_multiple_agents=${OCF_RESKEY_multiple_agents_default}}
: ${OCF_RESKEY_external_bridge=${OCF_RESKEY_external_bridge_default}}
: ${OCF_RESKEY_debug=${OCF_RESKEY_debug_default}}
: ${OCF_RESKEY_rescheduling_tries=${OCF_RESKEY_rescheduling_tries_default}}
: ${OCF_RESKEY_rescheduling_interval=${OCF_RESKEY_rescheduling_interval_default}}
: ${OCF_RESKEY_log_file=${OCF_RESKEY_log_file_default}}
#######################################################################
usage() {
cat <<UEND
usage: $0 (start|stop|validate-all|meta-data|status|monitor)
$0 manages an OpenStack L3 Service (neutron-vpn-agent) process as an HA resource
The 'start' operation starts the networking service.
The 'stop' operation stops the networking service.
The 'validate-all' operation reports whether the parameters are valid
The 'meta-data' operation reports this RA's meta-data information
The 'status' operation reports whether the networking service is running
The 'monitor' operation reports whether the networking service seems to be working
UEND
}
meta_data() {
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="neutron-vpn-agent">
<version>1.0</version>
<longdesc lang="en">
Resource agent for the OpenStack Router (neutron-vpn-agent)
May manage a neutron-vpn-agent instance or a clone set that
creates a distributed neutron-vpn-agent cluster.
</longdesc>
<shortdesc lang="en">Manages the OpenStack L3 Service (neutron-vpn-agent)</shortdesc>
<parameters>
<parameter name="binary" unique="0" required="0">
<longdesc lang="en">
Location of the OpenStack Router server binary (neutron-vpn-agent)
</longdesc>
<shortdesc lang="en">OpenStack Router server binary (neutron-vpn-agent)</shortdesc>
<content type="string" default="${OCF_RESKEY_binary_default}" />
</parameter>
<parameter name="config" unique="0" required="0">
<longdesc lang="en">
Location of the OpenStack Router (neutron-server) configuration file
</longdesc>
<shortdesc lang="en">OpenStack Router (neutron-server) config file</shortdesc>
<content type="string" default="${OCF_RESKEY_config_default}" />
</parameter>
<parameter name="keystone_config" unique="0" required="0">
<longdesc lang="en">
Location of the Keystone configuration file
</longdesc>
<shortdesc lang="en">OpenStack Keystone config file</shortdesc>
<content type="string" default="${OCF_RESKEY_keystone_config_default}" />
</parameter>
<parameter name="plugin_config" unique="0" required="0">
<longdesc lang="en">
Location of the OpenStack L3 Service (neutron-l3-agent) configuration file
</longdesc>
<shortdesc lang="en">OpenStack Router (neutron-l3-agent) config file</shortdesc>
<content type="string" default="${OCF_RESKEY_plugin_config_default}" />
</parameter>
<parameter name="vpn_config" unique="0" required="0">
<longdesc lang="en">
Location of the OpenStack L3 Service (neutron-vpn-agent) configuration file
</longdesc>
<shortdesc lang="en">OpenStack Router (neutron-vpn-agent) config file</shortdesc>
<content type="string" default="${OCF_RESKEY_vpn_config_default}" />
</parameter>
<parameter name="user" unique="0" required="0">
<longdesc lang="en">
User running OpenStack L3 Service (neutron-vpn-agent)
</longdesc>
<shortdesc lang="en">OpenStack L3 Service (neutron-vpn-agent) user</shortdesc>
<content type="string" default="${OCF_RESKEY_user_default}" />
</parameter>
<parameter name="pid" unique="0" required="0">
<longdesc lang="en">
The pid file to use for this OpenStack L3 Service (neutron-vpn-agent) instance
</longdesc>
<shortdesc lang="en">OpenStack L3 Service (neutron-vpn-agent) pid file</shortdesc>
<content type="string" default="${OCF_RESKEY_pid_default}" />
</parameter>
<parameter name="multiple_agents" unique="0" required="0">
<longdesc lang="en">
Flag, that switch RCS-agent behavior for multiple or single L3-agent.
</longdesc>
<shortdesc lang="en">Switsh between multiple or single L3-agent behavior</shortdesc>
<content type="string" default="${OCF_RESKEY_multiple_agents_default}" />
</parameter>
<parameter name="log_file" unique="0" required="0">
<longdesc lang="en">
The log file to use for this OpenStack L3 Service (neutron-vpn-agent) instance
</longdesc>
<shortdesc lang="en">OpenStack L3 Service (neutron-vpn-agent) log file</shortdesc>
<content type="string" default="${OCF_RESKEY_log_file_default}" />
</parameter>
<parameter name="neutron_server_port" unique="0" required="0">
<longdesc lang="en">
The listening port number of the AMQP server. Mandatory to perform a monitor check
</longdesc>
<shortdesc lang="en">AMQP listening port</shortdesc>
<content type="integer" default="${OCF_RESKEY_neutron_server_port_default}" />
</parameter>
<parameter name="username" unique="0" required="0">
<longdesc lang="en">
Neutron username for port list fetching
</longdesc>
<shortdesc lang="en">Neutron username</shortdesc>
<content type="string" default="${OCF_RESKEY_username_default}" />
</parameter>
<parameter name="password" unique="0" required="0">
<longdesc lang="en">
Neutron password for port list fetching
</longdesc>
<shortdesc lang="en">Neutron password</shortdesc>
<content type="string" default="${OCF_RESKEY_password_default}" />
</parameter>
<parameter name="os_auth_url" unique="0" required="0">
<longdesc lang="en">
URL of keystone
</longdesc>
<shortdesc lang="en">Keystone URL</shortdesc>
<content type="string" default="${OCF_RESKEY_os_auth_url_default}" />
</parameter>
<parameter name="tenant" unique="0" required="0">
<longdesc lang="en">
Admin tenant name
</longdesc>
<shortdesc lang="en">Admin tenant</shortdesc>
<content type="string" default="${OCF_RESKEY_tenant_default}" />
</parameter>
<parameter name="external_bridge" unique="0" required="0">
<longdesc lang="en">
External bridge for vpn-agent
</longdesc>
<shortdesc lang="en">External bridge</shortdesc>
<content type="string" />
</parameter>
<parameter name="debug" unique="0" required="0">
<longdesc lang="en">
Enable debug logging
</longdesc>
<shortdesc lang="en">Enable debug logging</shortdesc>
<content type="boolean" default="false"/>
</parameter>
<parameter name="rescheduling_tries" unique="0" required="0">
<longdesc lang="en">
Tries to start rescheduling script after start of agent.
</longdesc>
<shortdesc lang="en">Tries to start rescheduling script after start of agent.</shortdesc>
<content type="boolean" default="${OCF_RESKEY_rescheduling_tries_default}"/>
</parameter>
<parameter name="rescheduling_interval" unique="0" required="0">
<longdesc lang="en">
Interval between starts of rescheduling script.
</longdesc>
<shortdesc lang="en">Interval between starts of rescheduling script.</shortdesc>
<content type="boolean" default="${OCF_RESKEY_rescheduling_interval_default}"/>
</parameter>
<parameter name="syslog" unique="0" required="0">
<longdesc lang="en">
Enable logging to syslog
</longdesc>
<shortdesc lang="en">Enable logging to syslog</shortdesc>
<content type="boolean" default="false"/>
</parameter>
<parameter name="additional_parameters" unique="0" required="0">
<longdesc lang="en">
Additional parameters to pass on to the OpenStack L3 Service (neutron-vpn-agent)
</longdesc>
<shortdesc lang="en">Additional parameters for neutron-vpn-agent</shortdesc>
<content type="string" />
</parameter>
</parameters>
<actions>
<action name="start" timeout="20" />
<action name="stop" timeout="20" />
<action name="status" timeout="20" />
<action name="monitor" timeout="30" interval="20" />
<action name="validate-all" timeout="5" />
<action name="meta-data" timeout="5" />
</actions>
</resource-agent>
END
}
get_worker_pid() {
pid=`pgrep -u ${OCF_RESKEY_user} -fol ${OCF_RESKEY_binary} | grep -E "python\s+\/usr\/bin" | awk '{print $1}'`
echo $pid
}
#######################################################################
# Functions invoked by resource manager actions
neutron_l3_agent_validate() {
local rc
check_binary $OCF_RESKEY_binary
check_binary netstat
# A config file on shared storage that is not available
# during probes is OK.
if [ ! -f $OCF_RESKEY_config ]; then
if ! ocf_is_probe; then
ocf_log err "Config $OCF_RESKEY_config doesn't exist"
return $OCF_ERR_INSTALLED
fi
ocf_log_warn "Config $OCF_RESKEY_config not available during a probe"
fi
getent passwd $OCF_RESKEY_user >/dev/null 2>&1
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "User $OCF_RESKEY_user doesn't exist"
return $OCF_ERR_INSTALLED
fi
true
}
setup_auth() {
# setup token-based authentication if it possible
AUTH_TOKEN=""
if [[ -f $OCF_RESKEY_keystone_config ]] ; then
AUTH_TOKEN=$(grep -v '#' $OCF_RESKEY_keystone_config | grep -i 'admin_token\s*=\s*' | awk -F'=' '{print $2}')
fi
AUTH_TAIL=""
if [[ -n "$AUTH_TOKEN" ]] ; then
AUTH_TAIL="--admin-auth-url=${OCF_RESKEY_os_auth_url} --auth-token=${AUTH_TOKEN}"
fi
true
}
neutron_l3_agent_status() {
local pid
local f_pid
local rc
# check and make PID file dir
local PID_DIR="$( dirname ${OCF_RESKEY_pid} )"
if [ ! -d "${PID_DIR}" ] ; then
ocf_log debug "Create pid file dir: ${PID_DIR} and chown to ${OCF_RESKEY_user}"
mkdir -p "${PID_DIR}"
chown -R ${OCF_RESKEY_user} "${PID_DIR}"
chmod 755 "${PID_DIR}"
fi
pid=`get_worker_pid`
if [ "xxx$pid" == "xxx" ] ; then
ocf_log warn "OpenStack Neutron agent '$OCF_RESKEY_binary' not running."
return $OCF_NOT_RUNNING
fi
#ocf_log debug "PID='$pid'"
# Check PID file and create if need
if [ ! -f $OCF_RESKEY_pid ] ; then
ocf_log warn "OpenStack Neutron agent (${OCF_RESKEY_binary}) was run, but no PID file found."
ocf_log warn "Writing PID='$pid' to '$OCF_RESKEY_pid' for '${OCF_RESKEY_binary}' worker..."
echo $pid > $OCF_RESKEY_pid
return $OCF_SUCCESS
fi
# compare PID from file with PID from `pgrep...`
f_pid=`cat $OCF_RESKEY_pid | tr '\n' ' ' | awk '{print $1}'`
if [ "xxx$pid" == "xxx$f_pid" ]; then
return $OCF_SUCCESS
fi
# at this point we have PID file and PID from it
# differents with PID from `pgrep...`
if [ ! -d "/proc/$f_pid" ] || [ "xxx$f_pid" == "xxx" ] ; then
# process with PID from PID-file not found
ocf_log warn "Old PID file $OCF_RESKEY_pid found, but no running processes with PID=$f_pid found."
ocf_log warn "PID-file will be re-created (with PID=$pid)."
echo $pid > $OCF_RESKEY_pid
return $OCF_SUCCESS
fi
# at this point we have alien PID-file and running prosess with this PID.
ocf_log warn "Another daemon (with PID=$f_pid) running with PID file '$OCF_RESKEY_pid'. My PID=$pid"
return $OCF_ERR_GENERIC
}
get_ns_list() {
local rv=`ip netns list | grep -Ee "^qrouter-.*"`
echo $rv
}
get_pid_list_for_ns_list() {
# the first parameter is a list of ns names for searching pids
local ns_list="$1"
local pids=`for netns in $ns_list ; do ip netns exec $netns lsof -n -i -t ; done`
echo $pids
}
clean_up() {
# kill processes inside network namespaces
ns_list=`get_ns_list`
# kill all proceses from all dhcp-agent's net.namespaces, that using ip
count=3 # we will try kill process 3 times
while [ $count -gt 0 ]; do
# we can't use ps, because ps can't select processes for given network namespace
inside_ns_pids=`get_pid_list_for_ns_list "$ns_list"`
if [ -z "$inside_ns_pids" ] ; then
break
fi
for ns_pid in $inside_ns_pids ; do
ocf_run kill $ns_pid
done
sleep 1
count=$(($count - 1))
done
# kill all remaining proceses, that not died by simple kill
inside_ns_pids=`get_pid_list_for_ns_list "$ns_list"`
if [ ! -z "$inside_ns_pids" ] ; then
for ns_pid in $inside_ns_pids ; do
ocf_run kill -9 $ns_pid
done
fi
# cleanup network interfaces
q-agent-cleanup.py --agent=l3 --cleanup-ports
}
clean_up_namespaces() {
# kill unnided network namespaces.
#
# Be carefully. In each network namespace shouldn't be any processes
# using network!!! use clean_up before it
ns_list=`get_ns_list`
if [ ! -z "$ns_list" ] ; then
for ns_name in $ns_list ; do
ocf_run ip --force netns del $ns_name
done
fi
}
neutron_l3_agent_monitor() {
neutron_l3_agent_status
rc=$?
return $rc
}
neutron_l3_agent_start() {
local rc
neutron_l3_agent_status
rc=$?
if [ $rc -eq $OCF_SUCCESS ]; then
ocf_log info "OpenStack neutron-l3-agent already running"
return $OCF_SUCCESS
fi
clean_up
sleep 1
clean_up_namespaces
# run and detach to background agent as daemon.
# Don't use ocf_run as we're sending the tool's output to /dev/null
su ${OCF_RESKEY_user} -s /bin/sh -c "${OCF_RESKEY_binary} --config-file=$OCF_RESKEY_config \
--config-file=$OCF_RESKEY_plugin_config --config-file=$OCF_RESKEY_vpn_config --log-file=$OCF_RESKEY_log_file $OCF_RESKEY_additional_parameters \
>> /dev/null"' 2>&1 & echo \$! > $OCF_RESKEY_pid'
ocf_log debug "Create pid file: ${OCF_RESKEY_pid} with content $(cat ${OCF_RESKEY_pid})"
# Spin waiting for the server to come up.
# Let the CRM/LRM time us out if required
while true; do
neutron_l3_agent_monitor
rc=$?
[ $rc -eq $OCF_SUCCESS ] && break
if [ $rc -ne $OCF_NOT_RUNNING ] ; then
ocf_log err "OpenStack neutron-l3-agent start failed"
exit $OCF_ERR_GENERIC
fi
sleep 3
done
if ! ocf_is_true "$OCF_RESKEY_multiple_agents" ; then
# detach deferred rescheduling procedure
RESCHEDULING_CMD="q-agent-cleanup.py --agent=l3 --reschedule --remove-dead ${AUTH_TAIL} 2>&1 >> /var/log/neutron/rescheduling.log"
RESCH_CMD=''
for ((i=0; i<$OCF_RESKEY_rescheduling_tries; i++)) ; do
RESCH_CMD="$RESCH_CMD sleep $OCF_RESKEY_rescheduling_interval ; $RESCHEDULING_CMD ;"
done
bash -c "$RESCH_CMD" &
fuel-fdb-cleaner --ssh-keyfile /root/.ssh/id_rsa_neutron -l /var/log/neutron/fdb-cleaner.log
fi
ocf_log info "OpenStack Router (neutron-l3-agent) started"
return $OCF_SUCCESS
}
neutron_l3_agent_stop() {
local rc
local pid
neutron_l3_agent_status
rc=$?
if [ $rc -eq $OCF_NOT_RUNNING ]; then
clean_up
sleep 1
clean_up_namespaces
ocf_log info "OpenStack Router ($OCF_RESKEY_binary) already stopped"
return $OCF_SUCCESS
fi
# Try SIGTERM
pid=`get_worker_pid`
if [ "xxx$pid" == "xxx" ] ; then
ocf_log warn "OpenStack Router ($OCF_RESKEY_binary) not running."
#return $OCF_NOT_RUNNING
return $OCF_SUCCESS
fi
ocf_run kill -s TERM $pid
rc=$?
if [ $rc -ne 0 ]; then
ocf_log err "OpenStack Router ($OCF_RESKEY_binary) couldn't be stopped"
exit $OCF_ERR_GENERIC
fi
# stop waiting
shutdown_timeout=15
if [ -n "$OCF_RESKEY_CRM_meta_timeout" ]; then
shutdown_timeout=$((($OCF_RESKEY_CRM_meta_timeout/1000)-5))
fi
count=0
while [ $count -lt $shutdown_timeout ]; do
neutron_l3_agent_status
rc=$?
if [ $rc -eq $OCF_NOT_RUNNING ]; then
break
fi
count=`expr $count + 1`
sleep 1
ocf_log debug "OpenStack Router ($OCF_RESKEY_binary) still hasn't stopped yet. Waiting ..."
done
neutron_l3_agent_status
rc=$?
if [ $rc -ne $OCF_NOT_RUNNING ]; then
# SIGTERM didn't help either, try SIGKILL
ocf_log info "OpenStack Router ($OCF_RESKEY_binary) failed to stop after ${shutdown_timeout}s \
using SIGTERM. Trying SIGKILL ..."
ocf_run kill -s KILL $pid
fi
ocf_log info "OpenStack Router ($OCF_RESKEY_binary) stopped"
ocf_log debug "Delete pid file: ${OCF_RESKEY_pid} with content $(cat ${OCF_RESKEY_pid})"
rm -f $OCF_RESKEY_pid
clean_up
sleep 1
clean_up_namespaces
if ! ocf_is_true "$OCF_RESKEY_multiple_agents" ; then
echo ok >> /var/log/neutron/rescheduling.log &
q-agent-cleanup.py --agent=l3 --remove-self ${AUTH_TAIL} 2>&1 >> /var/log/neutron/rescheduling.log &
fi
sleep 3
return $OCF_SUCCESS
}
#######################################################################
case "$1" in
meta-data) meta_data
exit $OCF_SUCCESS;;
usage|help) usage
exit $OCF_SUCCESS;;
esac
# Anything except meta-data and help must pass validation
neutron_l3_agent_validate || exit $?
setup_auth || exit $?
# What kind of method was invoked?
case "$1" in
start) neutron_l3_agent_start;;
stop) neutron_l3_agent_stop;;
status) neutron_l3_agent_status;;
monitor) neutron_l3_agent_monitor;;
validate-all) ;;
*) usage
exit $OCF_ERR_UNIMPLEMENTED;;
esac

View File

@ -0,0 +1,600 @@
#!/usr/bin/env python
import re
import time
import os
import sys
import random
import string
import json
import argparse
import logging
import logging.handlers
import shlex
import subprocess
import StringIO
import socket
from neutronclient.neutron import client as q_client
from keystoneclient.v2_0 import client as ks_client
from keystoneclient.apiclient.exceptions import NotFound as ks_NotFound
LOG_NAME = 'q-agent-cleanup'
API_VER = '2.0'
PORT_ID_PART_LEN = 11
TMP_USER_NAME = 'tmp_neutron_admin'
def get_authconfig(cfg_file):
# Read OS auth config file
rv = {}
stripchars=" \'\""
with open(cfg_file) as f:
for line in f:
rg = re.match(r'\s*export\s+(\w+)\s*=\s*(.*)',line)
if rg :
#Use shlex to unescape bash shell escape characters
value = "".join(x for x in
shlex.split(rg.group(2).strip(stripchars)))
rv[rg.group(1).strip(stripchars)] = value
return rv
class NeutronCleaner(object):
PORT_NAME_PREFIXES_BY_DEV_OWNER = {
'network:dhcp': 'tap',
'network:router_gateway': 'qg-',
'network:router_interface': 'qr-',
}
PORT_NAME_PREFIXES = {
# contains tuples of prefixes
'dhcp': (PORT_NAME_PREFIXES_BY_DEV_OWNER['network:dhcp'],),
'l3': (
PORT_NAME_PREFIXES_BY_DEV_OWNER['network:router_gateway'],
PORT_NAME_PREFIXES_BY_DEV_OWNER['network:router_interface']
)
}
BRIDGES_FOR_PORTS_BY_AGENT ={
'dhcp': ('br-int',),
'l3': ('br-int', 'br-ex'),
}
PORT_OWNER_PREFIXES = {
'dhcp': ('network:dhcp',),
'l3': ('network:router_gateway', 'network:router_interface')
}
NS_NAME_PREFIXES = {
'dhcp': 'qdhcp',
'l3': 'qrouter',
}
AGENT_BINARY_NAME = {
'dhcp': 'neutron-dhcp-agent',
'l3': 'neutron-vpn-agent',
'ovs': 'neutron-openvswitch-agent'
}
CMD__list_ovs_port = ['ovs-vsctl', 'list-ports']
CMD__remove_ovs_port = ['ovs-vsctl', '--', '--if-exists', 'del-port']
CMD__remove_ip_addr = ['ip', 'address', 'delete']
CMD__ip_netns_list = ['ip', 'netns', 'list']
CMD__ip_netns_exec = ['ip', 'netns', 'exec']
RE__port_in_portlist = re.compile(r"^\s*\d+\:\s+([\w-]+)\:") # 14: tap-xxxyyyzzz:
def __init__(self, openrc, options, log=None):
self.log = log
self.auth_config = openrc
self.options = options
self.agents = {}
self.debug = options.get('debug')
self.RESCHEDULING_CALLS = {
'dhcp': self._reschedule_agent_dhcp,
'l3': self._reschedule_agent_l3,
}
self._token = None
self._keystone = None
self._client = None
self._need_cleanup_tmp_admin = False
def __del__(self):
if self._need_cleanup_tmp_admin and self._keystone and self._keystone.username:
try:
self._keystone.users.delete(self._keystone.users.find(username=self._keystone.username))
except:
# if we get exception while cleaning temporary account -- nothing harm
pass
def generate_random_passwd(self, length=13):
chars = string.ascii_letters + string.digits + '!@#$%^&*()'
random.seed = (os.urandom(1024))
return ''.join(random.choice(chars) for i in range(length))
@property
def keystone(self):
if self._keystone is None:
ret_count = self.options.get('retries', 1)
tmp_passwd = self.generate_random_passwd()
while True:
if ret_count <= 0:
self.log.error(">>> Keystone error: no more retries for connect to keystone server.")
sys.exit(1)
try:
a_token = self.options.get('auth-token')
a_url = self.options.get('admin-auth-url')
if a_token and a_url:
self.log.debug("Authentication by predefined token.")
# create keystone instance, authorized by service token
ks = ks_client.Client(
token=a_token,
endpoint=a_url,
)
service_tenant = ks.tenants.find(name='services')
auth_url = ks.endpoints.find(
service_id=ks.services.find(type='identity').id
).internalurl
# find and re-create temporary rescheduling-admin user with random password
try:
user = ks.users.find(username=TMP_USER_NAME)
ks.users.delete(user)
except ks_NotFound:
# user not found, it's OK
pass
user = ks.users.create(TMP_USER_NAME, tmp_passwd, tenant_id=service_tenant.id)
ks.roles.add_user_role(user, ks.roles.find(name='admin'), service_tenant)
# authenticate newly-created tmp neutron admin
self._keystone = ks_client.Client(
username=user.username,
password=tmp_passwd,
tenant_id=user.tenantId,
auth_url=auth_url,
)
self._need_cleanup_tmp_admin = True
else:
self.log.debug("Authentication by given credentionals.")
self._keystone = ks_client.Client(
username=self.auth_config['OS_USERNAME'],
password=self.auth_config['OS_PASSWORD'],
tenant_name=self.auth_config['OS_TENANT_NAME'],
auth_url=self.auth_config['OS_AUTH_URL'],
)
break
except Exception as e:
errmsg = str(e.message).strip() # str() need, because keystone may use int as message in exception
if re.search(r"Connection\s+refused$", errmsg, re.I) or \
re.search(r"Connection\s+timed\s+out$", errmsg, re.I) or\
re.search(r"Lost\s+connection\s+to\s+MySQL\s+server", errmsg, re.I) or\
re.search(r"Service\s+Unavailable$", errmsg, re.I) or\
re.search(r"'*NoneType'*\s+object\s+has\s+no\s+attribute\s+'*__getitem__'*$", errmsg, re.I) or \
re.search(r"No\s+route\s+to\s+host$", errmsg, re.I):
self.log.info(">>> Can't connect to {0}, wait for server ready...".format(self.auth_config['OS_AUTH_URL']))
time.sleep(self.options.sleep)
else:
self.log.error(">>> Keystone error:\n{0}".format(e.message))
raise e
ret_count -= 1
return self._keystone
@property
def token(self):
if self._token is None:
self._token = self._keystone.auth_token
#self.log.debug("Auth_token: '{0}'".format(self._token))
#todo: Validate existing token
return self._token
@property
def client(self):
if self._client is None:
self._client = q_client.Client(
API_VER,
endpoint_url=self.keystone.endpoints.find(
service_id=self.keystone.services.find(type='network').id
).adminurl,
token=self.token,
)
return self._client
def _neutron_API_call(self, method, *args):
ret_count = self.options.get('retries')
while True:
if ret_count <= 0:
self.log.error("Q-server error: no more retries for connect to server.")
return []
try:
rv = method (*args)
break
except Exception as e:
errmsg = str(e.message).strip()
if re.search(r"Connection\s+refused", errmsg, re.I) or\
re.search(r"Connection\s+timed\s+out", errmsg, re.I) or\
re.search(r"Lost\s+connection\s+to\s+MySQL\s+server", errmsg, re.I) or\
re.search(r"503\s+Service\s+Unavailable", errmsg, re.I) or\
re.search(r"No\s+route\s+to\s+host", errmsg, re.I):
self.log.info("Can't connect to {0}, wait for server ready...".format(self.keystone.service_catalog.url_for(service_type='network')))
time.sleep(self.options.sleep)
else:
self.log.error("Neutron error:\n{0}".format(e.message))
raise e
ret_count -= 1
return rv
def _get_agents(self,use_cache=True):
return self._neutron_API_call(self.client.list_agents)['agents']
def _get_routers(self, use_cache=True):
return self._neutron_API_call(self.client.list_routers)['routers']
def _get_networks(self, use_cache=True):
return self._neutron_API_call(self.client.list_networks)['networks']
def _list_networks_on_dhcp_agent(self, agent_id):
return self._neutron_API_call(self.client.list_networks_on_dhcp_agent, agent_id)['networks']
def _list_routers_on_l3_agent(self, agent_id):
return self._neutron_API_call(self.client.list_routers_on_l3_agent, agent_id)['routers']
def _list_l3_agents_on_router(self, router_id):
return self._neutron_API_call(self.client.list_l3_agent_hosting_routers, router_id)['agents']
def _list_dhcp_agents_on_network(self, network_id):
return self._neutron_API_call(self.client.list_dhcp_agent_hosting_networks, network_id)['agents']
def _list_orphaned_networks(self):
networks = self._get_networks()
self.log.debug("_list_orphaned_networks:, got list of networks {0}".format(json.dumps(networks,indent=4)))
orphaned_networks = []
for network in networks:
if len(self._list_dhcp_agents_on_network(network['id'])) == 0:
orphaned_networks.append(network['id'])
self.log.debug("_list_orphaned_networks:, got list of orphaned networks {0}".format(orphaned_networks))
return orphaned_networks
def _list_orphaned_routers(self):
routers = self._get_routers()
self.log.debug("_list_orphaned_routers:, got list of routers {0}".format(json.dumps(routers,indent=4)))
orphaned_routers = []
for router in routers:
if len(self._list_l3_agents_on_router(router['id'])) == 0:
orphaned_routers.append(router['id'])
self.log.debug("_list_orphaned_routers:, got list of orphaned routers {0}".format(orphaned_routers))
return orphaned_routers
def _add_network_to_dhcp_agent(self, agent_id, net_id):
return self._neutron_API_call(self.client.add_network_to_dhcp_agent, agent_id, {"network_id": net_id})
def _add_router_to_l3_agent(self, agent_id, router_id):
return self._neutron_API_call(self.client.add_router_to_l3_agent, agent_id, {"router_id": router_id})
def _remove_router_from_l3_agent(self, agent_id, router_id):
return self._neutron_API_call(self.client.remove_router_from_l3_agent, agent_id, router_id)
def _get_agents_by_type(self, agent, use_cache=True):
self.log.debug("_get_agents_by_type: start.")
rv = self.agents.get(agent, []) if use_cache else []
if not rv:
agents = self._get_agents(use_cache=use_cache)
for i in agents:
if i['binary'] == self.AGENT_BINARY_NAME.get(agent):
rv.append(i)
from_cache = ''
else:
from_cache = ' from local cache'
self.log.debug("_get_agents_by_type: end, {0} rv: {1}".format(from_cache, json.dumps(rv, indent=4)))
return rv
def __collect_namespaces_for_agent(self, agent):
cmd = self.CMD__ip_netns_list[:]
self.log.debug("Execute command '{0}'".format(' '.join(cmd)))
process = subprocess.Popen(
cmd,
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
rc = process.wait()
if rc != 0:
self.log.error("ERROR (rc={0}) while execution {1}".format(rc, ' '.join(cmd)))
return []
# filter namespaces by given agent type
netns = []
stdout = process.communicate()[0]
for ns in StringIO.StringIO(stdout):
ns = ns.strip()
self.log.debug("Found network namespace '{0}'".format(ns))
if ns.startswith("{0}-".format(self.NS_NAME_PREFIXES[agent])):
netns.append(ns)
return netns
def __collect_ports_for_namespace(self, ns):
cmd = self.CMD__ip_netns_exec[:]
cmd.extend([ns, 'ip', 'l', 'show'])
self.log.debug("Execute command '{0}'".format(' '.join(cmd)))
process = subprocess.Popen(
cmd,
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
rc = process.wait()
if rc != 0:
self.log.error("ERROR (rc={0}) while execution {1}".format(rc, ' '.join(cmd)))
return []
ports = []
stdout = process.communicate()[0]
for line in StringIO.StringIO(stdout):
pp = self.RE__port_in_portlist.match(line)
if not pp:
continue
port = pp.group(1)
if port != 'lo':
self.log.debug("Found port '{0}'".format(port))
ports.append(port)
return ports
def _cleanup_ports(self, agent):
self.log.debug("_cleanup_ports: start.")
# get namespaces list
netns = self.__collect_namespaces_for_agent(agent)
# collect ports from namespace
ports = []
for ns in netns:
ports.extend(self.__collect_ports_for_namespace(ns))
# iterate by port_list and remove port from OVS
for port in ports:
cmd = self.CMD__remove_ovs_port[:]
cmd.append(port)
if self.options.get('noop'):
self.log.info("NOOP-execution: '{0}'".format(' '.join(cmd)))
else:
self.log.debug("Execute command '{0}'".format(' '.join(cmd)))
process = subprocess.Popen(
cmd,
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
rc = process.wait()
if rc != 0:
self.log.error("ERROR (rc={0}) while execution {1}".format(rc, ' '.join(cmd)))
self.log.debug("_cleanup_ports: end.")
return True
def _reschedule_agent_dhcp(self, agent_type):
self.log.debug("_reschedule_agent_dhcp: start.")
agents = {
'alive': [],
'dead': []
}
# collect networklist from dead DHCP-agents
dead_networks = []
for agent in self._get_agents_by_type(agent_type):
if agent['alive']:
self.log.info("found alive DHCP agent: {0}".format(agent['id']))
agents['alive'].append(agent)
else:
# dead agent
self.log.info("found dead DHCP agent: {0}".format(agent['id']))
agents['dead'].append(agent)
for net in self._list_networks_on_dhcp_agent(agent['id']):
dead_networks.append(net)
if dead_networks and agents['alive']:
# get network-ID list of already attached to alive agent networks
lucky_ids = set()
map(
lambda net: lucky_ids.add(net['id']),
self._list_networks_on_dhcp_agent(agents['alive'][0]['id'])
)
# add dead networks to alive agent
for net in dead_networks:
if net['id'] not in lucky_ids:
# attach network to agent
self.log.info("attach network {net} to DHCP agent {agent}".format(
net=net['id'],
agent=agents['alive'][0]['id']
))
if not self.options.get('noop'):
self._add_network_to_dhcp_agent(agents['alive'][0]['id'], net['id'])
#if error:
# return
# remove dead agents if need (and if found alive agent)
if self.options.get('remove-dead'):
for agent in agents['dead']:
self.log.info("remove dead DHCP agent: {0}".format(agent['id']))
if not self.options.get('noop'):
self._neutron_API_call(self.client.delete_agent, agent['id'])
orphaned_networks=self._list_orphaned_networks()
self.log.info("_reschedule_agent_dhcp: rescheduling orphaned networks")
if orphaned_networks and agents['alive']:
for network in orphaned_networks:
self.log.info("_reschedule_agent_dhcp: rescheduling {0} to {1}".format(network,agents['alive'][0]['id']))
if not self.options.get('noop'):
self._add_network_to_dhcp_agent(agents['alive'][0]['id'], network)
self.log.info("_reschedule_agent_dhcp: ended rescheduling of orphaned networks")
self.log.debug("_reschedule_agent_dhcp: end.")
def _reschedule_agent_l3(self, agent_type):
self.log.debug("_reschedule_agent_l3: start.")
agents = {
'alive': [],
'dead': []
}
# collect router-list from dead DHCP-agents
dead_routers = [] # array of tuples (router, agentID)
for agent in self._get_agents_by_type(agent_type):
if agent['alive']:
self.log.info("found alive L3 agent: {0}".format(agent['id']))
agents['alive'].append(agent)
else:
# dead agent
self.log.info("found dead L3 agent: {0}".format(agent['id']))
agents['dead'].append(agent)
map(
lambda rou: dead_routers.append((rou, agent['id'])),
self._list_routers_on_l3_agent(agent['id'])
)
self.log.debug("L3 agents in cluster: {ags}".format(ags=json.dumps(agents, indent=4)))
self.log.debug("Routers, attached to dead L3 agents: {rr}".format(rr=json.dumps(dead_routers, indent=4)))
if dead_routers and agents['alive']:
# get router-ID list of already attached to alive agent routerss
lucky_ids = set()
map(
lambda rou: lucky_ids.add(rou['id']),
self._list_routers_on_l3_agent(agents['alive'][0]['id'])
)
# remove dead agents after rescheduling
for agent in agents['dead']:
self.log.info("remove dead L3 agent: {0}".format(agent['id']))
if not self.options.get('noop'):
self._neutron_API_call(self.client.delete_agent, agent['id'])
# move routers from dead to alive agent
for rou in filter(lambda rr: not(rr[0]['id'] in lucky_ids), dead_routers):
# self.log.info("unschedule router {rou} from L3 agent {agent}".format(
# rou=rou[0]['id'],
# agent=rou[1]
# ))
# if not self.options.get('noop'):
# self._remove_router_from_l3_agent(rou[1], rou[0]['id'])
# #todo: if error:
# #
self.log.info("schedule router {rou} to L3 agent {agent}".format(
rou=rou[0]['id'],
agent=agents['alive'][0]['id']
))
if not self.options.get('noop'):
self._add_router_to_l3_agent(agents['alive'][0]['id'], rou[0]['id'])
orphaned_routers=self._list_orphaned_routers()
self.log.info("_reschedule_agent_l3: rescheduling orphaned routers")
if orphaned_routers and agents['alive']:
for router in orphaned_routers:
self.log.info("_reschedule_agent_l3: rescheduling {0} to {1}".format(router,agents['alive'][0]['id']))
if not self.options.get('noop'):
self._add_router_to_l3_agent(agents['alive'][0]['id'], router)
self.log.info("_reschedule_agent_l3: ended rescheduling of orphaned routers")
self.log.debug("_reschedule_agent_l3: end.")
def _remove_self(self,agent_type):
self.log.debug("_remove_self: start.")
for agent in self._get_agents_by_type(agent_type):
if agent['host'] == socket.gethostname():
self.log.info("_remove_self: deleting our own agent {0} of type {1}".format(agent['id'],agent_type))
if not self.options.get('noop'):
self._neutron_API_call(self.client.delete_agent, agent['id'])
self.log.debug("_remove_self: end.")
def _reschedule_agent(self, agent):
self.log.debug("_reschedule_agents: start.")
task = self.RESCHEDULING_CALLS.get(agent, None)
if task:
task (agent)
self.log.debug("_reschedule_agents: end.")
def do(self, agent):
if self.options.get('cleanup-ports'):
self._cleanup_ports(agent)
if self.options.get('reschedule'):
self._reschedule_agent(agent)
if self.options.get('remove-self'):
self._remove_self(agent)
# if self.options.get('remove-agent'):
# self._cleanup_agents(agent)
def _test_healthy(self, agent_list, hostname):
rv = False
for agent in agent_list:
if agent['host'] == hostname and agent['alive']:
return True
return rv
def test_healthy(self, agent_type):
rc = 9 # OCF_FAILED_MASTER, http://www.linux-ha.org/doc/dev-guides/_literal_ocf_failed_master_literal_9.html
agentlist = self._get_agents_by_type(agent_type)
for hostname in self.options.get('test-hostnames'):
if self._test_healthy(agentlist, hostname):
return 0
return rc
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Neutron network node cleaning tool.')
parser.add_argument("-c", "--auth-config", dest="authconf", default="/root/openrc",
help="Authenticating config FILE", metavar="FILE")
parser.add_argument("-t", "--auth-token", dest="auth-token", default=None,
help="Authenticating token (instead username/passwd)", metavar="TOKEN")
parser.add_argument("-u", "--admin-auth-url", dest="admin-auth-url", default=None,
help="Authenticating URL (admin)", metavar="URL")
parser.add_argument("--retries", dest="retries", type=int, default=50,
help="try NN retries for API call", metavar="NN")
parser.add_argument("--sleep", dest="sleep", type=int, default=2,
help="sleep seconds between retries", metavar="SEC")
parser.add_argument("-a", "--agent", dest="agent", action="append",
help="specyfy agents for cleaning", required=True)
parser.add_argument("--cleanup-ports", dest="cleanup-ports", action="store_true", default=False,
help="cleanup ports for given agents on this node")
parser.add_argument("--remove-self", dest="remove-self", action="store_true", default=False,
help="remove ourselves from agent list")
parser.add_argument("--activeonly", dest="activeonly", action="store_true", default=False,
help="cleanup only active ports")
parser.add_argument("--reschedule", dest="reschedule", action="store_true", default=False,
help="reschedule given agents")
parser.add_argument("--remove-dead", dest="remove-dead", action="store_true", default=False,
help="remove dead agents while rescheduling")
parser.add_argument("--test-alive-for-hostname", dest="test-hostnames", action="append",
help="testing agent's healthy for given hostname")
parser.add_argument("--external-bridge", dest="external-bridge", default="br-ex",
help="external bridge name", metavar="IFACE")
parser.add_argument("--integration-bridge", dest="integration-bridge", default="br-int",
help="integration bridge name", metavar="IFACE")
parser.add_argument("-l", "--log", dest="log", action="store",
help="log file or logging.conf location")
parser.add_argument("--noop", dest="noop", action="store_true", default=False,
help="do not execute, print to log instead")
parser.add_argument("--debug", dest="debug", action="store_true", default=False,
help="debug")
args = parser.parse_args()
# if len(args) != 1:
# parser.error("incorrect number of arguments")
# parser.print_help() args = parser.parse_args()
#setup logging
if args.debug:
_log_level = logging.DEBUG
else:
_log_level = logging.INFO
if not args.log:
# log config or file not given -- log to console
LOG = logging.getLogger(LOG_NAME) # do not move to UP of file
_log_handler = logging.StreamHandler(sys.stdout)
_log_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
LOG.addHandler(_log_handler)
LOG.setLevel(_log_level)
elif args.log.split(os.sep)[-1] == 'logging.conf':
# setup logging by external file
import logging.config
logging.config.fileConfig(args.log)
LOG = logging.getLogger(LOG_NAME) # do not move to UP of file
else:
# log to given file
LOG = logging.getLogger(LOG_NAME) # do not move to UP of file
LOG.addHandler(logging.handlers.WatchedFileHandler(args.log))
LOG.setLevel(_log_level)
LOG.info("Started: {0}".format(' '.join(sys.argv)))
cleaner = NeutronCleaner(get_authconfig(args.authconf), options=vars(args), log=LOG)
rc = 0
if vars(args).get('test-hostnames'):
rc = cleaner.test_healthy(args.agent[0])
else:
for i in args.agent:
cleaner.do(i)
LOG.debug("End.")
sys.exit(rc)
#
###

View File

@ -0,0 +1,5 @@
Facter.add('cluster_mode') do
setcode do
Facter::Util::Resolution.exec("cat /etc/astute.yaml | grep deployment_mode | awk '{print $2}'")
end
end

View File

@ -0,0 +1,22 @@
Puppet::Type.type(:neutron_config).provide(
:ini_setting,
:parent => Puppet::Type.type(:ini_setting).provider(:ruby)
) do
def section
resource[:name].split('/', 2).first
end
def setting
resource[:name].split('/', 2).last
end
def separator
'='
end
def file_path
'/etc/neutron/neutron.conf'
end
end

View File

@ -0,0 +1,22 @@
Puppet::Type.type(:neutron_vpnaas_agent_config).provide(
:ini_setting,
:parent => Puppet::Type.type(:ini_setting).provider(:ruby)
) do
def section
resource[:name].split('/', 2).first
end
def setting
resource[:name].split('/', 2).last
end
def separator
'='
end
def file_path
'/etc/neutron/vpn_agent.ini'
end
end

View File

@ -0,0 +1,47 @@
Puppet::Type.newtype(:neutron_config) do
ensurable
newparam(:name, :namevar => true) do
desc 'Section/setting name to manage from neutron.conf'
newvalues(/\S+\/\S+/)
end
newproperty(:value) do
desc 'The value of the setting to be defined.'
munge do |value|
value = value.to_s.strip
value.capitalize! if value =~ /^(true|false)$/i
value
end
def is_to_s( currentvalue )
if resource.secret?
return '[old secret redacted]'
else
return currentvalue
end
end
def should_to_s( newvalue )
if resource.secret?
return '[new secret redacted]'
else
return newvalue
end
end
end
newparam(:secret, :boolean => true) do
desc 'Whether to hide the value from Puppet logs. Defaults to `false`.'
newvalues(:true, :false)
defaultto false
end
def create
provider.create
end
end

View File

@ -0,0 +1,18 @@
Puppet::Type.newtype(:neutron_vpnaas_agent_config) do
ensurable
newparam(:name, :namevar => true) do
desc 'Section/setting name to manage from vpn_agent.ini'
newvalues(/\S+\/\S+/)
end
newproperty(:value) do
desc 'The value of the setting to be defined.'
munge do |value|
value = value.to_s.strip
value.capitalize! if value =~ /^(true|false)$/i
value
end
end
end

View File

@ -0,0 +1,34 @@
#This class contains common changes both for HA and simple deployment mode.
#It enables VPN tab in Horizon and setups Neutron server.
class vpnaas::common {
include vpnaas::params
service { $vpnaas::params::dashboard_service:
ensure => running,
enable => true,
}
exec { "enable_vpnaas_dashboard":
command => "/bin/sed -i \"s/'enable_vpn': False/'enable_vpn': True/\" $vpnaas::params::dashboard_settings",
unless => "/bin/egrep \"'enable_vpn': True\" $vpnaas::params::dashboard_settings",
}
service { $vpnaas::params::server_service:
ensure => running,
enable => true,
}
neutron_config {
'DEFAULT/service_plugins': value => 'router,vpnaas,metering';
}
service { $vpnaas::params::ipsec_service:
ensure => running,
enable => true,
}
Neutron_config<||> ~> Service[$vpnaas::params::server_service]
Exec['enable_vpnaas_dashboard'] ~> Service[$vpnaas::params::dashboard_service]
}

View File

@ -0,0 +1,140 @@
#This class is intended to deploy VPNaaS in HA mode.
class vpnaas::ha {
include vpnaas::params
include neutron::params
$fuel_settings = parseyaml($astute_settings_yaml)
$access_hash = $fuel_settings['access']
$neutron_config = $fuel_settings['quantum_settings']
$multiple_agents = true
$primary_controller = $fuel_settings['role'] ? { 'primary-controller'=>true, default=>false }
$debug = true
$verbose = true
$syslog = $::use_syslog
$plugin_config = '/etc/neutron/l3_agent.ini'
file {'q-agent-cleanup.py':
path => '/usr/bin/q-agent-cleanup.py',
mode => '0755',
owner => root,
group => root,
source => "puppet:///modules/vpnaas/q-agent-cleanup.py",
}
class {'vpnaas::agent':
manage_service => true,
enabled => false,
}
if $primary_controller {
exec { "remove-l3-agent":
path => "/sbin:/usr/bin:/usr/sbin:/bin",
command => "pcs resource delete p_neutron-l3-agent --wait=120",
onlyif => "pcs resource show p_neutron-l3-agent > /dev/null 2>&1",
}
Exec['remove-l3-agent'] -> Class['vpnaas::agent']
}
else {
exec {'waiting-for-l3-deletion':
tries => 5,
try_sleep => 30,
command => "pcs resource show p_neutron-l3-agent > /dev/null 2>&1",
path => '/usr/sbin:/usr/bin:/sbin:/bin',
returns => [1],
}
Exec['waiting-for-l3-deletion'] -> Class['vpnaas::agent']
}
if $multiple_agents {
$csr_metadata = undef
$csr_multistate_hash = { 'type' => 'clone' }
$csr_ms_metadata = { 'interleave' => 'true' }
} else {
$csr_metadata = { 'resource-stickiness' => '1' }
$csr_multistate_hash = undef
$csr_ms_metadata = undef
}
$vpnaas_agent_package = $::neutron::params::vpnaas_agent_package ? {
false => $::neutron::params::package_name,
default => $::neutron::params::vpnaas_agent_package,
}
cluster::corosync::cs_service {'vpn':
ocf_script => 'neutron-agent-vpn',
csr_parameters => {
'debug' => $debug,
'syslog' => $syslog,
'plugin_config' => $plugin_config,
'os_auth_url' => "http://${fuel_settings['management_vip']}:35357/v2.0/",
'tenant' => 'services',
'username' => undef,
'password' => $neutron_config['keystone']['admin_password'],
'multiple_agents' => $multiple_agents,
},
csr_metadata => $csr_metadata,
csr_multistate_hash => $csr_multistate_hash,
csr_ms_metadata => $csr_ms_metadata,
csr_mon_intr => '20',
csr_mon_timeout => '10',
csr_timeout => '60',
service_name => $::neutron::params::vpnaas_agent_service,
package_name => $vpnaas_agent_package,
service_title => 'neutron-vpnaas-service',
primary => $primary_controller,
hasrestart => false,
}
cluster::corosync::cs_with_service {'vpn-and-ovs':
first => "clone_p_${neutron::params::ovs_agent_service}",
second => $multiple_agents ? {
false => "p_${neutron::params::vpnaas_agent_service}",
default => "clone_p_${neutron::params::vpnaas_agent_service}"
},
}
if ! $multiple_agents {
cs_colocation { 'vpn-keepaway-dhcp':
ensure => present,
score => '-100',
primitives => [
"p_${neutron::params::dhcp_agent_service}",
"p_${neutron::params::vpnaas_agent_service}"
],
require => Cluster::Corosync::Cs_service['vpn'],
}
}
File['q-agent-cleanup.py'] -> Cluster::Corosync::Cs_service["vpn"]
File["${vpnaas::params::vpn_agent_ocf_file}"] -> Cluster::Corosync::Cs_service["vpn"] ->
Cluster::Corosync::Cs_with_service['vpn-and-ovs'] -> Class['vpnaas::common']
#fuel-plugins system doesn't have 'primary-controller' role so
#we have to separate controllers' deployment here using waiting cycles.
if ! $primary_controller {
exec {'waiting-for-vpn-agent':
tries => 10,
try_sleep => 30,
command => "pcs resource show p_neutron-vpn-agent > /dev/null 2>&1",
path => '/usr/sbin:/usr/bin:/sbin:/bin',
}
Exec['waiting-for-vpn-agent'] -> Cluster::Corosync::Cs_service["vpn"]
}
file { "${vpnaas::params::vpn_agent_ocf_file}":
mode => 644,
owner => root,
group => root,
source => "puppet:///modules/vpnaas/ocf/neutron-agent-vpn"
}
class {'vpnaas::common':}
}

View File

@ -0,0 +1,15 @@
#This class deploys VPNaaS in simple mode.
class vpnaas {
service { 'disable-neutron-l3-service':
ensure => stopped,
name => "neutron-l3-agent",
enable => false,
}
Service['disable-neutron-l3-service'] -> Class['vpnaas::agent']
class {'vpnaas::agent':}
class {'vpnaas::common':}
}

View File

@ -0,0 +1,39 @@
#This class contains necessary parameters for all other manifests
class vpnaas::params {
if($::osfamily == 'Redhat') {
$server_package = 'openstack-neutron'
$server_service = 'neutron-server'
$vpnaas_agent_package = 'openstack-neutron-vpn-agent'
$vpnaas_agent_service = 'neutron-vpn-agent'
$openswan_package = 'openswan'
$dashboard_package = 'openstack-dashboard'
$dashboard_service = 'httpd'
$dashboard_settings = '/etc/openstack-dashboard/local_settings'
} elsif($::osfamily == 'Debian') {
$server_package = 'neutron-server'
$server_service = 'neutron-server'
$vpnaas_agent_package = 'neutron-vpn-agent'
$vpnaas_agent_service = 'neutron-vpn-agent'
$openswan_package = 'openswan'
$dashboard_package = 'python-django-horizon'
$dashboard_service = 'apache2'
$dashboard_settings = '/etc/openstack-dashboard/local_settings.py'
} else {
fail("Unsupported osfamily ${::osfamily}")
}
$ipsec_service = 'ipsec'
$vpn_agent_ocf_file = '/etc/puppet/modules/cluster/files/ocf/neutron-agent-vpn'
$cleanup_script_file = '/etc/puppet/modules/cluster/files/q-agent-cleanup.py'
}

View File

@ -0,0 +1,113 @@
#
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
#
# Author: Emilien Macchi <emilien.macchi@enovance.com>
#
# 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.
#
# == Class: vpnaas::agent
#
# Setups Neutron VPN agent.
#
# === Parameters
#
# [*package_ensure*]
# (optional) Ensure state for package. Defaults to 'present'.
#
# [*enabled*]
# (optional) Enable state for service. Defaults to 'true'.
#
# [*manage_service*]
# (optional) Whether to start/stop the service
# Defaults to true
#
# [*vpn_device_driver*]
# (optional) Defaults to 'neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver'.
#
# [*interface_driver*]
# (optional) Defaults to 'neutron.agent.linux.interface.OVSInterfaceDriver'.
#
# [*external_network_bridge]
# (optional) Defaults to undef
#
# [*ipsec_status_check_interval*]
# (optional) Status check interval. Defaults to '60'.
#
class vpnaas::agent (
$package_ensure = present,
$enabled = true,
$manage_service = true,
$vpn_device_driver = 'neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver',
$interface_driver = 'neutron.agent.linux.interface.OVSInterfaceDriver',
$external_network_bridge = undef,
$ipsec_status_check_interval = '60',
) {
include vpnaas::params
Neutron_vpnaas_agent_config<||> ~> Service['neutron-vpnaas-service']
case $vpn_device_driver {
/\.OpenSwan/: {
Package['openswan'] -> Package<| title == 'neutron-vpnaas-agent' |>
package { 'openswan':
ensure => present,
name => $::vpnaas::params::openswan_package,
}
}
default: {
fail("Unsupported vpn_device_driver ${vpn_device_driver}")
}
}
# The VPNaaS agent loads both neutron.ini and its own file.
# This only lists config specific to the agent. neutron.ini supplies
# the rest.
neutron_vpnaas_agent_config {
'vpnagent/vpn_device_driver': value => $vpn_device_driver;
'ipsec/ipsec_status_check_interval': value => $ipsec_status_check_interval;
'DEFAULT/interface_driver': value => $interface_driver;
}
if ($external_network_bridge) {
neutron_vpnaas_agent_config {
'DEFAULT/external_network_bridge': value => $external_network_bridge;
}
} else {
neutron_vpnaas_agent_config {
'DEFAULT/external_network_bridge': ensure => absent;
}
}
if $::vpnaas::params::vpnaas_agent_package {
Package['neutron-vpnaas-agent'] -> Neutron_vpnaas_agent_config<||>
package { 'neutron-vpnaas-agent':
ensure => $package_ensure,
name => $::vpnaas::params::vpnaas_agent_package,
}
}
if $manage_service {
if $enabled {
$service_ensure = 'running'
} else {
$service_ensure = 'stopped'
}
}
service { 'neutron-vpnaas-service':
ensure => $service_ensure,
name => $::vpnaas::params::vpnaas_agent_service,
enable => $enabled,
}
}

View File

@ -0,0 +1,14 @@
{
"name": "vpnaas",
"version": "0.1.0",
"author": "Sergey Kolekonov",
"summary": "Module to manage vpnaas",
"license": "Apache 2.0",
"source": "",
"project_page": "skolekonov@mirantis.com",
"issues_url": "skolekonov@mirantis.com",
"dependencies": [
{"name":"puppetlabs-stdlib","version_requirement":">= 1.0.0"}
]
}

View File

@ -0,0 +1,7 @@
require 'spec_helper'
describe 'vpnaas' do
context 'with defaults for all parameters' do
it { should contain_class('vpnaas') }
end
end

View File

@ -0,0 +1 @@
require 'puppetlabs_spec_helper/module_spec_helper'

View File

@ -0,0 +1,12 @@
# The baseline for module testing used by Puppet Labs is that each manifest
# should have a corresponding test manifest that declares that class or defined
# type.
#
# Tests are then run by using puppet apply --noop (to check for compilation
# errors and view a log of events) or by fully applying the test in a virtual
# environment (to compare the resulting system state to the desired state).
#
# Learn more about module testing here:
# http://docs.puppetlabs.com/guides/tests_smoke.html
#
include vpnaas

View File

@ -0,0 +1 @@
include vpnaas::ha

6
environment_config.yaml Normal file
View File

@ -0,0 +1,6 @@
attributes:
# Show vpnaas only for neutron
metadata:
restrictions:
- condition: cluster:net_provider != 'neutron'
action: hide

26
metadata.yaml Normal file
View File

@ -0,0 +1,26 @@
# Plugin name
name: vpnaas-plugin
# Human-readable name for your plugin
title: VPNaaS plugin for Neutron
# Plugin version
version: 1.0.0
# Description
description: Neutron extension that introduces VPN feature set
# Required fuel version
fuel_version: ['6.0']
# The plugin is compatible with releases in the list
releases:
- os: ubuntu
version: 2014.2-6.0
mode: ['ha', 'multinode']
deployment_scripts_path: deployment_scripts/
repository_path: repositories/ubuntu
- os: centos
version: 2014.2-6.0
mode: ['ha', 'multinode']
deployment_scripts_path: deployment_scripts/
repository_path: repositories/centos
# Version of plugin package
package_version: '1.0.0'

5
pre_build_hook Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
# Add here any the actions which are required before plugin build
# like packages building, packages downloading from mirrors and so on.
# The script should return 0 if there were no errors.

View File

View File

Binary file not shown.

7
tasks.yaml Normal file
View File

@ -0,0 +1,7 @@
- role: ['controller']
stage: post_deployment
type: puppet
parameters:
puppet_manifest: puppet/manifests/site.pp
puppet_modules: puppet/modules:/etc/puppet/modules
timeout: 720