Move all nsxlib code and tests to vmware_nsxlib

Change-Id: I75533e713a680674368d16f0a7aeb4fdbffe3608
This commit is contained in:
Adit Sarfaty 2016-10-02 10:57:45 +03:00
parent f32c46d037
commit e9ddc3dd33
33 changed files with 5837 additions and 62 deletions

View File

@ -1,7 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_LOG_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./vmware_nsxlib/tests/unit} $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -3,3 +3,15 @@
# process, which may cause wedges in the gate later.
pbr>=1.6 # Apache-2.0
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
eventlet!=0.18.3,>=0.18.2 # MIT
netaddr!=0.7.16,>=0.7.13 # BSD
retrying!=1.3.0,>=1.2.3 # Apache-2.0
six>=1.9.0 # MIT
neutron-lib>=0.4.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.16.0 # Apache-2.0

260
run_tests.sh Normal file
View File

@ -0,0 +1,260 @@
#!/usr/bin/env bash
set -eu
function usage {
echo "Usage: $0 [OPTION]..."
echo "Run Neutron's test suite(s)"
echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)."
echo " -n, --no-recreate-db Don't recreate the test database."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -u, --update Update the virtual environment with any newer package versions"
echo " -p, --pep8 Just run PEP8 and HACKING compliance check"
echo " -8, --pep8-only-changed [<basecommit>]"
echo " Just run PEP8 and HACKING compliance check on files changed since HEAD~1 (or <basecommit>)"
echo " -P, --no-pep8 Don't run static code checks"
echo " -c, --coverage Generate coverage report"
echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger."
echo " -h, --help Print this usage message"
echo " --virtual-env-path <path> Location of the virtualenv directory"
echo " Default: \$(pwd)"
echo " --virtual-env-name <name> Name of the virtualenv directory"
echo " Default: .venv"
echo " --tools-path <dir> Location of the tools directory"
echo " Default: \$(pwd)"
echo ""
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
exit
}
function process_options {
i=1
while [ $i -le $# ]; do
case "${!i}" in
-h|--help) usage;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-s|--no-site-packages) no_site_packages=1;;
-r|--recreate-db) recreate_db=1;;
-n|--no-recreate-db) recreate_db=0;;
-f|--force) force=1;;
-u|--update) update=1;;
-p|--pep8) just_pep8=1;;
-8|--pep8-only-changed) just_pep8_changed=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
-d|--debug) debug=1;;
--virtual-env-path)
(( i++ ))
venv_path=${!i}
;;
--virtual-env-name)
(( i++ ))
venv_dir=${!i}
;;
--tools-path)
(( i++ ))
tools_path=${!i}
;;
-*) testopts="$testopts ${!i}";;
*) testargs="$testargs ${!i}"
esac
(( i++ ))
done
}
tool_path=${tools_path:-$(pwd)}
venv_path=${venv_path:-$(pwd)}
venv_dir=${venv_name:-.venv}
with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
force=0
no_site_packages=0
installvenvopts=
testargs=
testopts=
wrapper=""
just_pep8=0
just_pep8_changed=0
no_pep8=0
coverage=0
debug=0
recreate_db=1
update=0
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
process_options $@
# Make our paths available to other scripts we call
export venv_path
export venv_dir
export venv_name
export tools_dir
export venv=${venv_path}/${venv_dir}
if [ $no_site_packages -eq 1 ]; then
installvenvopts="--no-site-packages"
fi
function run_tests {
# Cleanup *pyc
${wrapper} find . -type f -name "*.pyc" -delete
if [ $debug -eq 1 ]; then
if [ "$testopts" = "" ] && [ "$testargs" = "" ]; then
# Default to running all tests if specific test is not
# provided.
testargs="discover ./vmware_nsxlib/tests"
fi
${wrapper} python -m testtools.run $testopts $testargs
# Short circuit because all of the testr and coverage stuff
# below does not make sense when running testtools.run for
# debugging purposes.
return $?
fi
if [ $coverage -eq 1 ]; then
TESTRTESTS="$TESTRTESTS --coverage"
else
TESTRTESTS="$TESTRTESTS --slowest"
fi
# Just run the test suites in current environment
set +e
testargs=`echo "$testargs" | sed -e's/^\s*\(.*\)\s*$/\1/'`
TESTRTESTS="$TESTRTESTS --testr-args='--subunit $testopts $testargs'"
OS_TEST_PATH=`echo $testargs|grep -o 'vmware_nsxlib\neutron\.tests[^[:space:]:]\+'|tr . /`
if [ -n "$OS_TEST_PATH" ]; then
os_test_dir=$(dirname "$OS_TEST_PATH")
else
os_test_dir=''
fi
if [ -d "$OS_TEST_PATH" ]; then
wrapper="OS_TEST_PATH=$OS_TEST_PATH $wrapper"
elif [ -d "$os_test_dir" ]; then
wrapper="OS_TEST_PATH=$os_test_dir $wrapper"
fi
echo "Running \`${wrapper} $TESTRTESTS\`"
bash -c "${wrapper} $TESTRTESTS | ${wrapper} subunit2pyunit"
RESULT=$?
set -e
copy_subunit_log
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
# Don't compute coverage for common code, which is tested elsewhere
${wrapper} coverage combine
${wrapper} coverage html --include='neutron/*' --omit='neutron/openstack/common/*' -d covhtml -i
fi
return $RESULT
}
function copy_subunit_log {
LOGNAME=`cat .testrepository/next-stream`
LOGNAME=$(($LOGNAME - 1))
LOGNAME=".testrepository/${LOGNAME}"
cp $LOGNAME subunit.log
}
function warn_on_flake8_without_venv {
if [ $never_venv -eq 1 ]; then
echo "**WARNING**:"
echo "Running flake8 without virtual env may miss OpenStack HACKING detection"
fi
}
function run_pep8 {
echo "Running flake8 ..."
warn_on_flake8_without_venv
${wrapper} flake8
}
function run_pep8_changed {
# NOTE(gilliard) We want use flake8 to check the entirety of every file that has
# a change in it. Unfortunately the --filenames argument to flake8 only accepts
# file *names* and there are no files named (eg) "nova/compute/manager.py". The
# --diff argument behaves surprisingly as well, because although you feed it a
# diff, it actually checks the file on disk anyway.
local target=${testargs:-HEAD~1}
local files=$(git diff --name-only $target | tr '\n' ' ')
echo "Running flake8 on ${files}"
warn_on_flake8_without_venv
diff -u --from-file /dev/null ${files} | ${wrapper} flake8 --diff
}
TESTRTESTS="python setup.py testr"
if [ $never_venv -eq 0 ]
then
# Remove the virtual environment if --force used
if [ $force -eq 1 ]; then
echo "Cleaning virtualenv..."
rm -rf ${venv}
fi
if [ $update -eq 1 ]; then
echo "Updating virtualenv..."
python tools/install_venv.py $installvenvopts
fi
if [ -e ${venv} ]; then
wrapper="${with_venv}"
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
python tools/install_venv.py $installvenvopts
wrapper="${with_venv}"
else
echo -e "No virtual environment found...create one? (Y/n) \c"
read use_ve
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
# Install the virtualenv and run the test suite in it
python tools/install_venv.py $installvenvopts
wrapper=${with_venv}
fi
fi
fi
fi
# Delete old coverage data from previous runs
if [ $coverage -eq 1 ]; then
${wrapper} coverage erase
fi
if [ $just_pep8 -eq 1 ]; then
run_pep8
exit
fi
if [ $just_pep8_changed -eq 1 ]; then
run_pep8_changed
exit
fi
if [ $recreate_db -eq 1 ]; then
rm -f tests.sqlite
fi
run_tests
# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
# not when we're running tests individually. To handle this, we need to
# distinguish between options (testopts), which begin with a '-', and
# arguments (testargs).
if [ -z "$testargs" ]; then
if [ $no_pep8 -eq 0 ]; then
run_pep8
fi
fi

View File

@ -5,13 +5,21 @@
hacking<0.12,>=0.11.0 # Apache-2.0
coverage>=3.6 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
mock>=2.0 # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
oslosphinx>=4.7.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testresources>=0.2.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
# releasenotes
testscenarios>=0.4 # Apache-2.0/BSD
WebTest>=2.0 # MIT
# This is needed for subunit-trace
tempest-lib>=0.14.0 # Apache-2.0
reno>=1.8.0 # Apache2
bandit>=1.1.0 # Apache-2.0
tempest>=12.1.0 # Apache-2.0
pylint==1.4.5 # GPLv2
requests-mock>=1.1 # Apache-2.0

0
tools/__init__.py Normal file
View File

9
tools/ostestr_compat_shim.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
# Copied from neutron/tools. Otherwise no units tests are found.
# preserve old behavior of using an arg as a regex when '--' is not present
case $@ in
(*--*) ostestr $@;;
('') ostestr;;
(*) ostestr --regex "$@"
esac

118
tox.ini
View File

@ -1,39 +1,109 @@
[tox]
minversion = 2.0
envlist = py34,py27,pypy,pep8
envlist = py35,py34,py27,pep8,docs
minversion = 1.6
skipsdist = True
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONWARNINGS=default::DeprecationWarning
passenv = TRACE_FAILONLY GENERATE_HASHES http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}'
install_command =
pip install -U -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
whitelist_externals = sh
commands =
{toxinidir}/tools/ostestr_compat_shim.sh {posargs}
# there is also secret magic in ostestr which lets you run in a fail only
# mode. To do this define the TRACE_FAILONLY environmental variable.
[testenv:common]
# Fake job to define environment variables shared between dsvm/non-dsvm jobs
setenv = OS_TEST_TIMEOUT=180
commands = false
[testenv:functional]
basepython = python2.7
setenv = {[testenv]setenv}
{[testenv:common]setenv}
OS_TEST_PATH=./vmware_nsxlib/tests/functional
OS_LOG_PATH={env:OS_LOG_PATH:/opt/stack/logs}
deps =
{[testenv]deps}
-r{toxinidir}/vmware_nsxlib/tests/functional/requirements.txt
[testenv:dsvm-functional]
basepython = python2.7
setenv = OS_SUDO_TESTING=1
OS_FAIL_ON_MISSING_DEPS=1
OS_TEST_TIMEOUT=180
sitepackages=True
deps =
{[testenv:functional]deps}
commands =
[tox:jenkins]
sitepackages = True
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:pep8]
commands = flake8 {posargs}
basepython = python2.7
deps =
{[testenv]deps}
commands =
# Checks for coding and style guidelines
flake8
{[testenv:genconfig]commands}
whitelist_externals =
sh
bash
[testenv:bandit]
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r vmware_nsxlib -n 5 -ll
[testenv:cover]
basepython = python2.7
commands =
python setup.py testr --coverage --testr-args='{posargs}'
coverage report
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:releasenotes]
commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:debug]
commands = oslo_debug_helper {posargs}
commands = sphinx-build -W -b html doc/source doc/build/html
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
# E125 continuation line does not distinguish itself from next logical line
# E126 continuation line over-indented for hanging indent
# E128 continuation line under-indented for visual indent
# E129 visually indented line with same indent as next logical line
# E265 block comment should start with #
# H305 imports not grouped correctly
# H307 like imports should be grouped together
# H402 one line docstring needs punctuation
# H404 multi line docstring should start with a summary
# H405 multi line docstring summary not separated with an empty line
# H904 Wrap long lines in parentheses instead of a backslash
# TODO(dougwig) -- uncomment this to test for remaining linkages
# N530 direct neutron imports not allowed
ignore = E125,E126,E128,E129,E265,H305,H307,H402,H404,H405,H904,N530
show-source = true
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,.ropeproject
[hacking]
import_exceptions = vmware_nsxlib._i18n,
vmware_nsxlib_tempest._i18n
local-check-factory = neutron_lib.hacking.checks.factory
[testenv:genconfig]
commands =
[testenv:uuidgen]
commands =
check-uuid --fix

43
vmware_nsxlib/_i18n.py Normal file
View File

@ -0,0 +1,43 @@
# Copyright (c) 2015 VMware, Inc.
# 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.
import oslo_i18n
DOMAIN = "vmware_nsxlib"
_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
# The primary translation function using the well-known name "_"
_ = _translators.primary
# The contextual translation function using the name "_C"
_C = _translators.contextual_form
# The plural translation function using the name "_P"
_P = _translators.plural_form
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
def get_available_languages():
return oslo_i18n.get_available_languages(DOMAIN)

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_vmware_nsxlib
----------------------------------
Tests for `vmware_nsxlib` module.
"""
from vmware_nsxlib.tests import base
class TestVmware_nsxlib(base.TestCase):
def test_something(self):
pass

View File

View File

View File

@ -0,0 +1,231 @@
# Copyright (c) 2015 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
import six.moves.urllib.parse as urlparse
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from vmware_nsxlib.v3 import nsx_constants
FAKE_NAME = "fake_name"
DEFAULT_TIER0_ROUTER_UUID = "efad0078-9204-4b46-a2d8-d4dd31ed448f"
NSX_BRIDGE_CLUSTER_NAME = 'default bridge cluster'
FAKE_MANAGER = "fake_manager_ip"
def make_fake_switch(switch_uuid=None, tz_uuid=None, name=FAKE_NAME):
if not switch_uuid:
switch_uuid = uuidutils.generate_uuid()
if not tz_uuid:
tz_uuid = uuidutils.generate_uuid()
fake_switch = {
"id": switch_uuid,
"display_name": name,
"resource_type": "LogicalSwitch",
"address_bindings": [],
"transport_zone_id": tz_uuid,
"replication_mode": nsx_constants.MTEP,
"admin_state": nsx_constants.ADMIN_STATE_UP,
"vni": 50056,
"switching_profile_ids": [
{
"value": "64814784-7896-3901-9741-badeff705639",
"key": "IpDiscoverySwitchingProfile"
},
{
"value": "fad98876-d7ff-11e4-b9d6-1681e6b88ec1",
"key": "SpoofGuardSwitchingProfile"
},
{
"value": "93b4b7e8-f116-415d-a50c-3364611b5d09",
"key": "PortMirroringSwitchingProfile"
},
{
"value": "fbc4fb17-83d9-4b53-a286-ccdf04301888",
"key": "SwitchSecuritySwitchingProfile"
},
{
"value": "f313290b-eba8-4262-bd93-fab5026e9495",
"key": "QosSwitchingProfile"
}
],
}
return fake_switch
def make_fake_dhcp_profile():
return {"id": uuidutils.generate_uuid(),
"edge_cluster_id": uuidutils.generate_uuid(),
"edge_cluster_member_indexes": [0, 1]}
def make_fake_metadata_proxy():
return {"id": uuidutils.generate_uuid(),
"metadata_server_url": "http://1.2.3.4",
"secret": "my secret",
"edge_cluster_id": uuidutils.generate_uuid(),
"edge_cluster_member_indexes": [0, 1]}
class MockRequestsResponse(object):
def __init__(self, status_code, content=None):
self.status_code = status_code
self.content = content
def json(self):
return jsonutils.loads(self.content)
class MockRequestSessionApi(object):
def __init__(self):
self._store = {}
def _format_uri(self, uri):
uri = urlparse.urlparse(uri).path
while uri.endswith('/'):
uri = uri[:-1]
while uri.startswith('/'):
uri = uri[1:]
if not self._is_uuid_uri(uri):
uri = "%s/" % uri
return uri
def _is_uuid_uri(self, uri):
return uuidutils.is_uuid_like(
urlparse.urlparse(uri).path.split('/')[-1])
def _query(self, search_key, copy=True):
items = []
for uri, obj in self._store.items():
if uri.startswith(search_key):
items.append(obj.copy() if copy else obj)
return items
def _build_response(self, url, content=None,
status=requests.codes.ok, **kwargs):
if isinstance(content, list):
content = {
'result_count': len(content),
'results': content
}
if (content is not None and kwargs.get('headers', {}).get(
'Content-Type') == 'application/json'):
content = jsonutils.dumps(content)
return MockRequestsResponse(status, content=content)
def _get_content(self, **kwargs):
content = kwargs.get('data', None)
if content and kwargs.get('headers', {}).get(
'Content-Type') == 'application/json':
content = jsonutils.loads(content)
return content
def get(self, url, **kwargs):
url = self._format_uri(url)
if self._is_uuid_uri(url):
item = self._store.get(url)
code = requests.codes.ok if item else requests.codes.not_found
return self._build_response(
url, content=item, status=code, **kwargs)
return self._build_response(
url, content=self._query(url), status=requests.codes.ok, **kwargs)
def _create(self, url, content, **kwargs):
resource_id = content.get('id')
if resource_id and self._store.get("%s%s" % (url, resource_id)):
return self._build_response(
url, content=None, status=requests.codes.bad, **kwargs)
resource_id = resource_id or uuidutils.generate_uuid()
content['id'] = resource_id
self._store["%s%s" % (url, resource_id)] = content.copy()
return content
def post(self, url, **kwargs):
parsed_url = urlparse.urlparse(url)
url = self._format_uri(url)
if self._is_uuid_uri(url):
if self._store.get(url) is None:
return self._build_response(
url, content=None, status=requests.codes.bad, **kwargs)
body = self._get_content(**kwargs)
if body is None:
return self._build_response(
url, content=None, status=requests.codes.bad, **kwargs)
response_content = None
url_queries = urlparse.parse_qs(parsed_url.query)
if 'create_multiple' in url_queries.get('action', []):
response_content = {}
for resource_name, resource_body in body.items():
for new_resource in resource_body:
created_resource = self._create(
url, new_resource, **kwargs)
if response_content.get(resource_name, None) is None:
response_content[resource_name] = []
response_content[resource_name].append(created_resource)
else:
response_content = self._create(url, body, **kwargs)
if isinstance(response_content, MockRequestsResponse):
return response_content
return self._build_response(
url, content=response_content,
status=requests.codes.created, **kwargs)
def put(self, url, **kwargs):
url = self._format_uri(url)
item = {}
if self._is_uuid_uri(url):
item = self._store.get(url, None)
if item is None:
return self._build_response(
url, content=None,
status=requests.codes.not_found, **kwargs)
body = self._get_content(**kwargs)
if body is None:
return self._build_response(
url, content=None, status=requests.codes.bad, **kwargs)
item.update(body)
self._store[url] = item
return self._build_response(
url, content=item, status=requests.codes.ok, **kwargs)
def delete(self, url, **kwargs):
url = self._format_uri(url)
if not self._store.get(url):
return self._build_response(
url, content=None, status=requests.codes.not_found, **kwargs)
del self._store[url]
return self._build_response(
url, content=None, status=requests.codes.ok, **kwargs)

View File

@ -0,0 +1,322 @@
# Copyright (c) 2015 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import copy
import mock
import unittest
from oslo_utils import uuidutils
from requests import exceptions as requests_exceptions
from vmware_nsxlib import v3
from vmware_nsxlib.v3 import client as nsx_client
from vmware_nsxlib.v3 import cluster as nsx_cluster
from vmware_nsxlib.v3 import config
NSX_USER = 'admin'
NSX_PASSWORD = 'default'
NSX_MANAGER = '1.2.3.4'
NSX_INSECURE = False
NSX_CERT = '/opt/stack/certs/nsx.pem'
NSX_HTTP_RETRIES = 10
NSX_HTTP_TIMEOUT = 10
NSX_HTTP_READ_TIMEOUT = 180
NSX_CONCURENT_CONN = 10
NSX_CONN_IDLE_TIME = 10
NSX_MAX_ATTEMPTS = 10
PLUGIN_SCOPE = "plugin scope"
PLUGIN_TAG = "plugin tag"
PLUGIN_VER = "plugin ver"
def _mock_nsxlib():
def _return_id_key(*args, **kwargs):
return {'id': uuidutils.generate_uuid()}
def _mock_add_rules_in_section(*args):
# NOTE(arosen): the code in the neutron plugin expects the
# neutron rule id as the display_name.
rules = args[0]
return {
'rules': [
{'display_name': rule['display_name'],
'id': uuidutils.generate_uuid()}
for rule in rules
]}
mock.patch(
"vmware_nsxlib.v3.cluster.NSXRequestsHTTPProvider"
".validate_connection").start()
mock.patch(
"vmware_nsxlib.v3.security.NsxLibNsGroup.create",
side_effect=_return_id_key
).start()
mock.patch(
"vmware_nsxlib.v3.security.NsxLibFirewallSection.create_empty",
side_effect=_return_id_key).start()
mock.patch(
"vmware_nsxlib.v3.security.NsxLibFirewallSection.init_default",
side_effect=_return_id_key).start()
mock.patch(
"vmware_nsxlib.v3.security.NsxLibNsGroup.list").start()
mock.patch(
"vmware_nsxlib.v3.security.NsxLibFirewallSection.add_rules",
side_effect=_mock_add_rules_in_section).start()
mock.patch(
"vmware_nsxlib.v3.NsxLibTransportZone.get_id_by_name_or_id",
side_effect=_return_id_key).start()
mock.patch(
"vmware_nsxlib.v3.NsxLib.get_version",
return_value='1.1.0').start()
def get_default_nsxlib_config():
return config.NsxLibConfig(
username=NSX_USER,
password=NSX_PASSWORD,
retries=NSX_HTTP_RETRIES,
insecure=NSX_INSECURE,
ca_file=NSX_CERT,
concurrent_connections=NSX_CONCURENT_CONN,
http_timeout=NSX_HTTP_TIMEOUT,
http_read_timeout=NSX_HTTP_READ_TIMEOUT,
conn_idle_timeout=NSX_CONN_IDLE_TIME,
http_provider=None,
nsx_api_managers=[],
plugin_scope=PLUGIN_SCOPE,
plugin_tag=PLUGIN_TAG,
plugin_ver=PLUGIN_VER)
class NsxLibTestCase(unittest.TestCase):
def setUp(self, *args, **kwargs):
super(NsxLibTestCase, self).setUp()
_mock_nsxlib()
nsxlib_config = get_default_nsxlib_config()
self.nsxlib = v3.NsxLib(nsxlib_config)
# print diffs when assert comparisons fail
self.maxDiff = None
class MemoryMockAPIProvider(nsx_cluster.AbstractHTTPProvider):
"""Acts as a HTTP provider for mocking which is backed
by a MockRequestSessionApi.
"""
def __init__(self, mock_session_api):
self._store = mock_session_api
@property
def provider_id(self):
return "Memory mock API"
def validate_connection(self, cluster_api, endpoint, conn):
return
def new_connection(self, cluster_api, provider):
# all callers use the same backing
return self._store
def is_connection_exception(self, exception):
return isinstance(exception, requests_exceptions.ConnectionError)
class NsxClientTestCase(NsxLibTestCase):
class MockNSXClusteredAPI(nsx_cluster.NSXClusteredAPI):
def __init__(
self, session_response=None,
username=None,
password=None,
retries=None,
insecure=None,
ca_file=None,
concurrent_connections=None,
http_timeout=None,
http_read_timeout=None,
conn_idle_timeout=None,
nsx_api_managers=None):
nsxlib_config = config.NsxLibConfig(
username=username or NSX_USER,
password=password or NSX_PASSWORD,
retries=retries or NSX_HTTP_RETRIES,
insecure=insecure if insecure is not None else NSX_INSECURE,
ca_file=ca_file or NSX_CERT,
concurrent_connections=(concurrent_connections or
NSX_CONCURENT_CONN),
http_timeout=http_timeout or NSX_HTTP_TIMEOUT,
http_read_timeout=http_read_timeout or NSX_HTTP_READ_TIMEOUT,
conn_idle_timeout=conn_idle_timeout or NSX_CONN_IDLE_TIME,
http_provider=NsxClientTestCase.MockHTTPProvider(
session_response=session_response),
nsx_api_managers=nsx_api_managers or [NSX_MANAGER],
plugin_scope=PLUGIN_SCOPE,
plugin_tag=PLUGIN_TAG,
plugin_ver=PLUGIN_VER)
super(NsxClientTestCase.MockNSXClusteredAPI, self).__init__(
nsxlib_config)
self._record = mock.Mock()
def record_call(self, request, **kwargs):
verb = request.method.lower()
# filter out requests specific attributes
checked_kwargs = copy.copy(kwargs)
del checked_kwargs['proxies']
del checked_kwargs['stream']
if 'allow_redirects' in checked_kwargs:
del checked_kwargs['allow_redirects']
for attr in ['url', 'body']:
checked_kwargs[attr] = getattr(request, attr, None)
# remove headers we don't need to verify
checked_kwargs['headers'] = copy.copy(request.headers)
for header in ['Accept-Encoding', 'User-Agent',
'Connection', 'Authorization',
'Content-Length']:
if header in checked_kwargs['headers']:
del checked_kwargs['headers'][header]
checked_kwargs['headers'] = request.headers
# record the call in the mock object
method = getattr(self._record, verb)
method(**checked_kwargs)
def assert_called_once(self, verb, **kwargs):
mock_call = getattr(self._record, verb.lower())
mock_call.assert_called_once_with(**kwargs)
@property
def recorded_calls(self):
return self._record
class MockHTTPProvider(nsx_cluster.NSXRequestsHTTPProvider):
def __init__(self, session_response=None):
super(NsxClientTestCase.MockHTTPProvider, self).__init__()
self._session_response = session_response
def new_connection(self, cluster_api, provider):
# wrapper the session so we can intercept and record calls
session = super(NsxClientTestCase.MockHTTPProvider,
self).new_connection(cluster_api, provider)
mock_adapter = mock.Mock()
session_send = session.send
def _adapter_send(request, **kwargs):
# record calls at the requests HTTP adapter level
mock_response = mock.Mock()
mock_response.history = None
# needed to bypass requests internal checks for mock
mock_response.raw._original_response = {}
# record the request for later verification
cluster_api.record_call(request, **kwargs)
return mock_response
def _session_send(request, **kwargs):
# calls at the Session level
if self._session_response:
# consumer has setup a response for the session
cluster_api.record_call(request, **kwargs)
return (self._session_response()
if hasattr(self._session_response, '__call__')
else self._session_response)
# bypass requests redirect handling for mock
kwargs['allow_redirects'] = False
# session send will end up calling adapter send
return session_send(request, **kwargs)
mock_adapter.send = _adapter_send
session.send = _session_send
def _mock_adapter(*args, **kwargs):
# use our mock adapter rather than requests adapter
return mock_adapter
session.get_adapter = _mock_adapter
return session
def validate_connection(self, cluster_api, endpoint, conn):
assert conn is not None
def mock_nsx_clustered_api(self, session_response=None, **kwargs):
return NsxClientTestCase.MockNSXClusteredAPI(
session_response=session_response, **kwargs)
def mocked_resource(self, resource_class, mock_validate=True,
session_response=None):
mocked = resource_class(nsx_client.NSX3Client(
self.mock_nsx_clustered_api(session_response=session_response),
max_attempts=NSX_MAX_ATTEMPTS))
if mock_validate:
mock.patch.object(mocked._client, '_validate_result').start()
return mocked
def new_mocked_client(self, client_class, mock_validate=True,
session_response=None, mock_cluster=None,
**kwargs):
client = client_class(mock_cluster or self.mock_nsx_clustered_api(
session_response=session_response), **kwargs)
if mock_validate:
mock.patch.object(client, '_validate_result').start()
new_client_for = client.new_client_for
def _new_client_for(*args, **kwargs):
sub_client = new_client_for(*args, **kwargs)
if mock_validate:
mock.patch.object(sub_client, '_validate_result').start()
return sub_client
client.new_client_for = _new_client_for
return client
def new_mocked_cluster(self, conf_managers, validate_conn_func,
concurrent_connections=None):
mock_provider = mock.Mock()
mock_provider.default_scheme = 'https'
mock_provider.validate_connection = validate_conn_func
nsxlib_config = get_default_nsxlib_config()
if concurrent_connections:
nsxlib_config.concurrent_connections = concurrent_connections
nsxlib_config.http_provider = mock_provider
nsxlib_config.nsx_api_managers = conf_managers
return nsx_cluster.NSXClusteredAPI(nsxlib_config)

View File

@ -0,0 +1,308 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import copy
from oslo_log import log
from oslo_serialization import jsonutils
from vmware_nsxlib.tests.unit.v3 import mocks
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.v3 import client
from vmware_nsxlib.v3 import exceptions as nsxlib_exc
LOG = log.getLogger(__name__)
CLIENT_PKG = 'vmware_nsxlib.v3.client'
DFT_ACCEPT_HEADERS = {
'Accept': '*/*'
}
def _headers(**kwargs):
headers = copy.copy(DFT_ACCEPT_HEADERS)
headers.update(kwargs)
return headers
def assert_call(verb, client_or_resource,
url, verify=nsxlib_testcase.NSX_CERT,
data=None, headers=DFT_ACCEPT_HEADERS,
timeout=(nsxlib_testcase.NSX_HTTP_TIMEOUT,
nsxlib_testcase.NSX_HTTP_READ_TIMEOUT)):
nsx_client = client_or_resource
if getattr(nsx_client, '_client', None) is not None:
nsx_client = nsx_client._client
cluster = nsx_client._conn
cluster.assert_called_once(
verb,
**{'url': url, 'verify': verify, 'body': data,
'headers': headers, 'cert': None, 'timeout': timeout})
def assert_json_call(verb, client_or_resource, url,
verify=nsxlib_testcase.NSX_CERT,
data=None,
headers=client.JSONRESTClient._DEFAULT_HEADERS):
return assert_call(verb, client_or_resource, url,
verify=verify, data=data,
headers=headers)
class NsxV3RESTClientTestCase(nsxlib_testcase.NsxClientTestCase):
def test_client_url_prefix(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='/cloud/api')
api.list()
assert_call(
'get', api,
'https://1.2.3.4/cloud/api')
api = self.new_mocked_client(client.RESTClient,
url_prefix='/cloud/api')
api.url_list('v1/ports')
assert_call(
'get', api,
'https://1.2.3.4/cloud/api/v1/ports')
def test_client_headers(self):
default_headers = {'Content-Type': 'application/golang'}
api = self.new_mocked_client(
client.RESTClient, default_headers=default_headers,
url_prefix='/v1/api')
api.list()
assert_call(
'get', api,
'https://1.2.3.4/v1/api',
headers=_headers(**default_headers))
api = self.new_mocked_client(
client.RESTClient,
default_headers=default_headers,
url_prefix='/v1/api')
method_headers = {'X-API-Key': 'strong-crypt'}
api.url_list('ports/33', headers=method_headers)
method_headers.update(default_headers)
assert_call(
'get', api,
'https://1.2.3.4/v1/api/ports/33',
headers=_headers(**method_headers))
def test_client_for(self):
api = self.new_mocked_client(client.RESTClient, url_prefix='api/v1/')
sub_api = api.new_client_for('switch/ports')
sub_api.get('11a2b')
assert_call(
'get', sub_api,
'https://1.2.3.4/api/v1/switch/ports/11a2b')
def test_client_list(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.list()
assert_call(
'get', api,
'https://1.2.3.4/api/v1/ports')
def test_client_get(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.get('unique-id')
assert_call(
'get', api,
'https://1.2.3.4/api/v1/ports/unique-id')
def test_client_delete(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.delete('unique-id')
assert_call(
'delete', api,
'https://1.2.3.4/api/v1/ports/unique-id')
def test_client_update(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.update('unique-id', jsonutils.dumps({'name': 'a-new-name'}))
assert_call(
'put', api,
'https://1.2.3.4/api/v1/ports/unique-id',
data=jsonutils.dumps({'name': 'a-new-name'}))
def test_client_create(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.create(body=jsonutils.dumps({'resource-name': 'port1'}))
assert_call(
'post', api,
'https://1.2.3.4/api/v1/ports',
data=jsonutils.dumps({'resource-name': 'port1'}))
def test_client_url_list(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
json_headers = {'Content-Type': 'application/json'}
api.url_list('/connections', json_headers)
assert_call(
'get', api,
'https://1.2.3.4/api/v1/ports/connections',
headers=_headers(**json_headers))
def test_client_url_get(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.url_get('connections/1')
assert_call(
'get', api,
'https://1.2.3.4/api/v1/ports/connections/1')
def test_client_url_delete(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.url_delete('1')
assert_call(
'delete', api,
'https://1.2.3.4/api/v1/ports/1')
def test_client_url_put(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.url_put('connections/1', jsonutils.dumps({'name': 'conn1'}))
assert_call(
'put', api,
'https://1.2.3.4/api/v1/ports/connections/1',
data=jsonutils.dumps({'name': 'conn1'}))
def test_client_url_post(self):
api = self.new_mocked_client(client.RESTClient,
url_prefix='api/v1/ports')
api.url_post('1/connections', jsonutils.dumps({'name': 'conn1'}))
assert_call(
'post', api,
'https://1.2.3.4/api/v1/ports/1/connections',
data=jsonutils.dumps({'name': 'conn1'}))
def test_client_validate_result(self):
def _verb_response_code(http_verb, status_code):
response = mocks.MockRequestsResponse(
status_code, None)
client_api = self.new_mocked_client(
client.RESTClient, mock_validate=False,
session_response=response)
client_call = getattr(client_api, "url_%s" % http_verb)
client_call('', None)
for verb in ['get', 'post', 'put', 'delete']:
for code in client.RESTClient._VERB_RESP_CODES.get(verb):
_verb_response_code(verb, code)
self.assertRaises(
nsxlib_exc.ManagerError,
_verb_response_code, verb, 500)
class NsxV3JSONClientTestCase(nsxlib_testcase.NsxClientTestCase):
def test_json_request(self):
resp = mocks.MockRequestsResponse(
200, jsonutils.dumps({'result': {'ok': 200}}))
api = self.new_mocked_client(client.JSONRESTClient,
session_response=resp,
url_prefix='api/v2/nat')
resp = api.create(body={'name': 'mgmt-egress'})
assert_json_call(
'post', api,
'https://1.2.3.4/api/v2/nat',
data=jsonutils.dumps({'name': 'mgmt-egress'}))
self.assertEqual(resp, {'result': {'ok': 200}})
class NsxV3APIClientTestCase(nsxlib_testcase.NsxClientTestCase):
def test_api_call(self):
api = self.new_mocked_client(client.NSX3Client)
api.get('ports')
assert_json_call(
'get', api,
'https://1.2.3.4/api/v1/ports')
# NOTE(boden): remove this when tmp brigding removed
class NsxV3APIClientBridgeTestCase(nsxlib_testcase.NsxClientTestCase):
def test_get_resource(self):
api = self.new_mocked_client(client.NSX3Client)
api.get('ports')
assert_json_call(
'get', api,
'https://1.2.3.4/api/v1/ports')
def test_create_resource(self):
api = self.new_mocked_client(client.NSX3Client)
api.create('ports', {'resource-name': 'port1'})
assert_json_call(
'post', api,
'https://1.2.3.4/api/v1/ports',
data=jsonutils.dumps({'resource-name': 'port1'}))
def test_update_resource(self):
api = self.new_mocked_client(client.NSX3Client)
api.update('ports/1', {'name': 'a-new-name'})
assert_json_call(
'put', api,
'https://1.2.3.4/api/v1/ports/1',
data=jsonutils.dumps({'name': 'a-new-name'}))
def test_delete_resource(self):
api = self.new_mocked_client(client.NSX3Client)
api.delete('ports/11')
assert_json_call(
'delete', api,
'https://1.2.3.4/api/v1/ports/11')

View File

@ -0,0 +1,205 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import mock
import six.moves.urllib.parse as urlparse
import unittest
from oslo_serialization import jsonutils
from requests import exceptions as requests_exceptions
from vmware_nsxlib.tests.unit.v3 import mocks
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.v3 import client
from vmware_nsxlib.v3 import cluster
from vmware_nsxlib.v3 import exceptions as nsxlib_exc
def _validate_conn_up(*args, **kwargs):
return
def _validate_conn_down(*args, **kwargs):
raise requests_exceptions.ConnectionError()
class RequestsHTTPProviderTestCase(unittest.TestCase):
def test_new_connection(self):
mock_api = mock.Mock()
mock_api.nsxlib_config = mock.Mock()
mock_api.nsxlib_config.username = 'nsxuser'
mock_api.nsxlib_config.password = 'nsxpassword'
mock_api.nsxlib_config.retries = 100
mock_api.nsxlib_config.insecure = True
mock_api.nsxlib_config.ca_file = None
mock_api.nsxlib_config.http_timeout = 99
mock_api.nsxlib_config.conn_idle_timeout = 39
provider = cluster.NSXRequestsHTTPProvider()
session = provider.new_connection(
mock_api, cluster.Provider('9.8.7.6', 'https://9.8.7.6',
'nsxuser', 'nsxpassword', None))
self.assertEqual(session.auth, ('nsxuser', 'nsxpassword'))
self.assertEqual(session.verify, False)
self.assertEqual(session.cert, None)
self.assertEqual(session.adapters['https://'].max_retries.total, 100)
self.assertEqual(session.timeout, 99)
def test_validate_connection(self):
self.skipTest("Revist")
mock_conn = mocks.MockRequestSessionApi()
mock_ep = mock.Mock()
mock_ep.provider.url = 'https://1.2.3.4'
provider = cluster.NSXRequestsHTTPProvider()
self.assertRaises(nsxlib_exc.ResourceNotFound,
provider.validate_connection,
mock.Mock(), mock_ep, mock_conn)
mock_conn.post('api/v1/transport-zones',
data=jsonutils.dumps({'id': 'dummy-tz'}),
headers=client.JSONRESTClient._DEFAULT_HEADERS)
provider.validate_connection(mock.Mock(), mock_ep, mock_conn)
class NsxV3ClusteredAPITestCase(nsxlib_testcase.NsxClientTestCase):
def _assert_providers(self, cluster_api, provider_tuples):
self.assertEqual(len(cluster_api.providers), len(provider_tuples))
def _assert_provider(pid, purl):
for provider in cluster_api.providers:
if provider.id == pid and provider.url == purl:
return
self.fail("Provider: %s not found" % pid)
for provider_tuple in provider_tuples:
_assert_provider(provider_tuple[0], provider_tuple[1])
def test_conf_providers_no_scheme(self):
conf_managers = ['8.9.10.11', '9.10.11.12:4433']
api = self.new_mocked_cluster(conf_managers, _validate_conn_up)
self._assert_providers(
api, [(p, "https://%s" % p) for p in conf_managers])
def test_conf_providers_with_scheme(self):
conf_managers = ['http://8.9.10.11:8080', 'https://9.10.11.12:4433']
api = self.new_mocked_cluster(conf_managers, _validate_conn_up)
self._assert_providers(
api, [(urlparse.urlparse(p).netloc, p) for p in conf_managers])
def test_http_retries(self):
api = self.mock_nsx_clustered_api(retries=9)
with api.endpoints['1.2.3.4'].pool.item() as session:
self.assertEqual(
session.adapters['https://'].max_retries.total, 9)
def test_conns_per_pool(self):
conf_managers = ['8.9.10.11', '9.10.11.12:4433']
api = self.new_mocked_cluster(
conf_managers, _validate_conn_up,
concurrent_connections=11)
for ep_id, ep in api.endpoints.items():
self.assertEqual(ep.pool.max_size, 11)
def test_timeouts(self):
api = self.mock_nsx_clustered_api(http_read_timeout=37, http_timeout=7)
api.get('logical-ports')
mock_call = api.recorded_calls.method_calls[0]
name, args, kwargs = mock_call
self.assertEqual(kwargs['timeout'], (7, 37))
class ClusteredAPITestCase(nsxlib_testcase.NsxClientTestCase):
def _test_health(self, validate_fn, expected_health):
conf_managers = ['8.9.10.11', '9.10.11.12']
api = self.new_mocked_cluster(conf_managers, validate_fn)
self.assertEqual(api.health, expected_health)
def test_orange_health(self):
def _validate(cluster_api, endpoint, conn):
if endpoint.provider.id == '8.9.10.11':
raise Exception()
self._test_health(_validate, cluster.ClusterHealth.ORANGE)
def test_green_health(self):
self._test_health(_validate_conn_up, cluster.ClusterHealth.GREEN)
def test_red_health(self):
self._test_health(_validate_conn_down, cluster.ClusterHealth.RED)
def test_cluster_validate_with_exception(self):
conf_managers = ['8.9.10.11', '9.10.11.12', '10.11.12.13']
api = self.new_mocked_cluster(conf_managers, _validate_conn_down)
self.assertEqual(len(api.endpoints), 3)
self.assertRaises(nsxlib_exc.ServiceClusterUnavailable,
api.get, 'api/v1/transport-zones')
def test_cluster_proxy_stale_revision(self):
def stale_revision():
raise nsxlib_exc.StaleRevision(manager='1.1.1.1',
operation='whatever')
api = self.mock_nsx_clustered_api(session_response=stale_revision)
self.assertRaises(nsxlib_exc.StaleRevision,
api.get, 'api/v1/transport-zones')
def test_cluster_proxy_connection_error(self):
def connect_timeout():
raise requests_exceptions.ConnectTimeout()
api = self.mock_nsx_clustered_api(session_response=connect_timeout)
api._validate = mock.Mock()
self.assertRaises(nsxlib_exc.ServiceClusterUnavailable,
api.get, 'api/v1/transport-zones')
def test_cluster_round_robin_servicing(self):
conf_managers = ['8.9.10.11', '9.10.11.12', '10.11.12.13']
api = self.mock_nsx_clustered_api(nsx_api_managers=conf_managers)
api._validate = mock.Mock()
eps = list(api._endpoints.values())
def _get_schedule(num_eps):
return [api._select_endpoint() for i in range(num_eps)]
self.assertEqual(_get_schedule(3), eps)
self.assertEqual(_get_schedule(6), [eps[0], eps[1], eps[2],
eps[0], eps[1], eps[2]])
eps[0]._state = cluster.EndpointState.DOWN
self.assertEqual(_get_schedule(4), [eps[1], eps[2], eps[1], eps[2]])
eps[1]._state = cluster.EndpointState.DOWN
self.assertEqual(_get_schedule(2), [eps[2], eps[2]])
eps[0]._state = cluster.EndpointState.UP
self.assertEqual(_get_schedule(4), [eps[0], eps[2], eps[0], eps[2]])
def test_reinitialize_cluster(self):
api = self.mock_nsx_clustered_api()
# just make sure this api is defined, and does not crash
api._reinit_cluster()

View File

@ -0,0 +1,162 @@
# Copyright (c) 2016 VMware, Inc.
#
# 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.
from oslo_utils import uuidutils
FAKE_NAME = "fake_name"
FAKE_SWITCH_UUID = uuidutils.generate_uuid()
FAKE_PORT_UUID = uuidutils.generate_uuid()
FAKE_PORT = {
"id": FAKE_PORT_UUID,
"display_name": FAKE_NAME,
"resource_type": "LogicalPort",
"address_bindings": [],
"logical_switch_id": FAKE_SWITCH_UUID,
"admin_state": "UP",
"attachment": {
"id": "9ca8d413-f7bf-4276-b4c9-62f42516bdb2",
"attachment_type": "VIF"
},
"switching_profile_ids": [
{
"value": "64814784-7896-3901-9741-badeff705639",
"key": "IpDiscoverySwitchingProfile"
},
{
"value": "fad98876-d7ff-11e4-b9d6-1681e6b88ec1",
"key": "SpoofGuardSwitchingProfile"
},
{
"value": "93b4b7e8-f116-415d-a50c-3364611b5d09",
"key": "PortMirroringSwitchingProfile"
},
{
"value": "fbc4fb17-83d9-4b53-a286-ccdf04301888",
"key": "SwitchSecuritySwitchingProfile"
},
{
"value": "f313290b-eba8-4262-bd93-fab5026e9495",
"key": "QosSwitchingProfile"
}
]
}
FAKE_CONTAINER_PORT = {
"id": FAKE_PORT_UUID,
"display_name": FAKE_NAME,
"resource_type": "LogicalPort",
"address_bindings": [
{
"ip_address": "192.168.1.110",
"mac_address": "aa:bb:cc:dd:ee:ff"
}
],
"logical_switch_id": FAKE_SWITCH_UUID,
"admin_state": "UP",
"attachment": {
"id": "9ca8d413-f7bf-4276-b4c9-62f42516bdb2",
"attachment_type": "CIF",
"context": {
"vlan_tag": 122,
"container_host_vif_id": "c6f817a0-4e36-421e-98a6-8a2faed880bc",
"key_values": [],
"resource_type": "CifAttachmentContext",
}
},
"switching_profile_ids": [
{
"value": "64814784-7896-3901-9741-badeff705639",
"key": "IpDiscoverySwitchingProfile"
},
{
"value": "fad98876-d7ff-11e4-b9d6-1681e6b88ec1",
"key": "SpoofGuardSwitchingProfile"
},
{
"value": "93b4b7e8-f116-415d-a50c-3364611b5d09",
"key": "PortMirroringSwitchingProfile"
},
{
"value": "fbc4fb17-83d9-4b53-a286-ccdf04301888",
"key": "SwitchSecuritySwitchingProfile"
},
{
"value": "f313290b-eba8-4262-bd93-fab5026e9495",
"key": "QosSwitchingProfile"
}
]
}
FAKE_ROUTER_UUID = uuidutils.generate_uuid()
FAKE_ROUTER = {
"resource_type": "LogicalRouter",
"revision": 0,
"id": FAKE_ROUTER_UUID,
"display_name": FAKE_NAME
}
FAKE_ROUTER_PORT_UUID = uuidutils.generate_uuid()
FAKE_ROUTER_PORT = {
"resource_type": "LogicalRouterLinkPort",
"revision": 0,
"id": FAKE_ROUTER_PORT_UUID,
"display_name": FAKE_NAME,
"logical_router_id": FAKE_ROUTER_UUID
}
FAKE_QOS_PROFILE = {
"resource_type": "QosSwitchingProfile",
"id": uuidutils.generate_uuid(),
"display_name": FAKE_NAME,
"system_defined": False,
"dscp": {
"priority": 25,
"mode": "UNTRUSTED"
},
"tags": [],
"description": FAKE_NAME,
"class_of_service": 0,
"shaper_configuration": [
{
"resource_type": "IngressRateShaper",
"enabled": False,
"peak_bandwidth_mbps": 0,
"burst_size_bytes": 0,
"average_bandwidth_mbps": 0
},
{
"resource_type": "IngressBroadcastRateShaper",
"enabled": False,
"peak_bandwidth_kbps": 0,
"average_bandwidth_kbps": 0,
"burst_size_bytes": 0
},
{
"resource_type": "EgressRateShaper",
"enabled": False,
"peak_bandwidth_mbps": 0,
"burst_size_bytes": 0,
"average_bandwidth_mbps": 0
}
],
"_last_modified_user": "admin",
"_last_modified_time": 1438383180608,
"_create_time": 1438383180608,
"_create_user": "admin",
"_revision": 0
}

View File

@ -0,0 +1,178 @@
# Copyright (c) 2015 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import mock
from oslo_log import log
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.tests.unit.v3 import test_constants
LOG = log.getLogger(__name__)
class NsxLibQosTestCase(nsxlib_testcase.NsxClientTestCase):
def _body(self, qos_marking=None, dscp=None,
description=test_constants.FAKE_NAME):
body = {
"resource_type": "QosSwitchingProfile",
"tags": []
}
if qos_marking:
body = self.nsxlib.qos_switching_profile._update_dscp_in_args(
body, qos_marking, dscp)
body["display_name"] = test_constants.FAKE_NAME
body["description"] = description
return body
def _body_with_shaping(self, shaping_enabled=False,
burst_size=None,
peak_bandwidth=None,
average_bandwidth=None,
description=test_constants.FAKE_NAME,
qos_marking=None,
dscp=0):
body = test_constants.FAKE_QOS_PROFILE
body["display_name"] = test_constants.FAKE_NAME
body["description"] = description
for shaper in body["shaper_configuration"]:
# We currently support only shaping of Egress traffic
if shaper["resource_type"] == "EgressRateShaper":
shaper["enabled"] = shaping_enabled
if burst_size:
shaper["burst_size_bytes"] = burst_size
if peak_bandwidth:
shaper["peak_bandwidth_mbps"] = peak_bandwidth
if average_bandwidth:
shaper["average_bandwidth_mbps"] = average_bandwidth
break
if qos_marking:
body = self.nsxlib.qos_switching_profile._update_dscp_in_args(
body, qos_marking, dscp)
return body
def test_create_qos_switching_profile(self):
"""Test creating a qos-switching profile
returns the correct response
"""
with mock.patch.object(self.nsxlib.client, 'create') as create:
self.nsxlib.qos_switching_profile.create(
tags=[],
name=test_constants.FAKE_NAME,
description=test_constants.FAKE_NAME)
create.assert_called_with(
'switching-profiles', self._body())
def test_update_qos_switching_profile(self):
"""Test updating a qos-switching profile
returns the correct response
"""
original_profile = self._body()
new_description = "Test"
with mock.patch.object(self.nsxlib.client, 'get',
return_value=original_profile):
with mock.patch.object(self.nsxlib.client, 'update') as update:
# update the description of the profile
self.nsxlib.qos_switching_profile.update(
test_constants.FAKE_QOS_PROFILE['id'],
tags=[],
description=new_description)
update.assert_called_with(
'switching-profiles/%s'
% test_constants.FAKE_QOS_PROFILE['id'],
self._body(description=new_description))
def test_enable_qos_switching_profile_shaping(self):
"""Test updating a qos-switching profile
returns the correct response
"""
original_profile = self._body_with_shaping()
burst_size = 100
peak_bandwidth = 200
average_bandwidth = 300
qos_marking = "untrusted"
dscp = 10
with mock.patch.object(self.nsxlib.client, 'get',
return_value=original_profile):
with mock.patch.object(self.nsxlib.client, 'update') as update:
# update the bw shaping of the profile
self.nsxlib.qos_switching_profile.update_shaping(
test_constants.FAKE_QOS_PROFILE['id'],
shaping_enabled=True,
burst_size=burst_size,
peak_bandwidth=peak_bandwidth,
average_bandwidth=average_bandwidth,
qos_marking=qos_marking,
dscp=dscp)
update.assert_called_with(
'switching-profiles/%s'
% test_constants.FAKE_QOS_PROFILE['id'],
self._body_with_shaping(
shaping_enabled=True,
burst_size=burst_size,
peak_bandwidth=peak_bandwidth,
average_bandwidth=average_bandwidth,
qos_marking="untrusted", dscp=10))
def test_disable_qos_switching_profile_shaping(self):
"""Test updating a qos-switching profile
returns the correct response
"""
burst_size = 100
peak_bandwidth = 200
average_bandwidth = 300
original_profile = self._body_with_shaping(
shaping_enabled=True,
burst_size=burst_size,
peak_bandwidth=peak_bandwidth,
average_bandwidth=average_bandwidth,
qos_marking="untrusted",
dscp=10)
with mock.patch.object(self.nsxlib.client, 'get',
return_value=original_profile):
with mock.patch.object(self.nsxlib.client, 'update') as update:
# update the bw shaping of the profile
self.nsxlib.qos_switching_profile.update_shaping(
test_constants.FAKE_QOS_PROFILE['id'],
shaping_enabled=False, qos_marking="trusted")
update.assert_called_with(
'switching-profiles/%s'
% test_constants.FAKE_QOS_PROFILE['id'],
self._body_with_shaping(qos_marking="trusted"))
def test_delete_qos_switching_profile(self):
"""Test deleting qos-switching-profile"""
with mock.patch.object(self.nsxlib.client, 'delete') as delete:
self.nsxlib.qos_switching_profile.delete(
test_constants.FAKE_QOS_PROFILE['id'])
delete.assert_called_with(
'switching-profiles/%s'
% test_constants.FAKE_QOS_PROFILE['id'])

View File

@ -0,0 +1,514 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import copy
import mock
from oslo_serialization import jsonutils
from vmware_nsxlib.tests.unit.v3 import mocks
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.tests.unit.v3 import test_client
from vmware_nsxlib.tests.unit.v3 import test_constants
from vmware_nsxlib.v3 import resources
CLIENT_PKG = test_client.CLIENT_PKG
profile_types = resources.SwitchingProfileTypes
class TestSwitchingProfileTestCase(nsxlib_testcase.NsxClientTestCase):
def _mocked_switching_profile(self, session_response=None):
return self.mocked_resource(
resources.SwitchingProfile, session_response=session_response)
def test_switching_profile_create(self):
mocked_resource = self._mocked_switching_profile()
mocked_resource.create(profile_types.PORT_MIRRORING,
'pm-profile', 'port mirror prof')
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles',
data=jsonutils.dumps({
'resource_type': profile_types.PORT_MIRRORING,
'display_name': 'pm-profile',
'description': 'port mirror prof'
}, sort_keys=True))
def test_switching_profile_update(self):
tags = [
{
'scope': 'os-project-id',
'tag': 'tenant-1'
},
{
'scope': 'os-api-version',
'tag': '2.1.1.0'
}
]
mocked_resource = self._mocked_switching_profile()
mocked_resource.update(
'a12bc1', profile_types.PORT_MIRRORING, tags=tags)
test_client.assert_json_call(
'put', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles/a12bc1',
data=jsonutils.dumps({
'resource_type': profile_types.PORT_MIRRORING,
'tags': tags
}, sort_keys=True))
def test_spoofgaurd_profile_create(self):
tags = [
{
'scope': 'os-project-id',
'tag': 'tenant-1'
},
{
'scope': 'os-api-version',
'tag': '2.1.1.0'
}
]
mocked_resource = self._mocked_switching_profile()
mocked_resource.create_spoofguard_profile(
'plugin-spoof', 'spoofguard-for-plugin',
whitelist_ports=True, tags=tags)
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles',
data=jsonutils.dumps({
'resource_type': profile_types.SPOOF_GUARD,
'display_name': 'plugin-spoof',
'description': 'spoofguard-for-plugin',
'white_list_providers': ['LPORT_BINDINGS'],
'tags': tags
}, sort_keys=True))
def test_create_dhcp_profile(self):
tags = [
{
'scope': 'os-project-id',
'tag': 'tenant-1'
},
{
'scope': 'os-api-version',
'tag': '2.1.1.0'
}
]
mocked_resource = self._mocked_switching_profile()
mocked_resource.create_dhcp_profile(
'plugin-dhcp', 'dhcp-for-plugin',
tags=tags)
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles',
data=jsonutils.dumps({
'bpdu_filter': {
'enabled': True,
'white_list': []
},
'resource_type': profile_types.SWITCH_SECURITY,
'display_name': 'plugin-dhcp',
'description': 'dhcp-for-plugin',
'tags': tags,
'dhcp_filter': {
'client_block_enabled': True,
'server_block_enabled': False
},
'rate_limits': {
'enabled': False,
'rx_broadcast': 0,
'tx_broadcast': 0,
'rx_multicast': 0,
'tx_multicast': 0
},
'block_non_ip_traffic': True
}, sort_keys=True))
def test_create_mac_learning_profile(self):
tags = [
{
'scope': 'os-project-id',
'tag': 'tenant-1'
},
{
'scope': 'os-api-version',
'tag': '2.1.1.0'
}
]
mocked_resource = self._mocked_switching_profile()
mocked_resource.create_mac_learning_profile(
'plugin-mac-learning', 'mac-learning-for-plugin',
tags=tags)
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles',
data=jsonutils.dumps({
'mac_learning': {
'enabled': True,
},
'resource_type': profile_types.MAC_LEARNING,
'display_name': 'plugin-mac-learning',
'description': 'mac-learning-for-plugin',
'tags': tags,
}, sort_keys=True))
def test_find_by_display_name(self):
resp_resources = {
'results': [
{'display_name': 'resource-1'},
{'display_name': 'resource-2'},
{'display_name': 'resource-3'}
]
}
session_response = mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources))
mocked_resource = self._mocked_switching_profile(
session_response=session_response)
self.assertEqual([{'display_name': 'resource-1'}],
mocked_resource.find_by_display_name('resource-1'))
self.assertEqual([{'display_name': 'resource-2'}],
mocked_resource.find_by_display_name('resource-2'))
self.assertEqual([{'display_name': 'resource-3'}],
mocked_resource.find_by_display_name('resource-3'))
resp_resources = {
'results': [
{'display_name': 'resource-1'},
{'display_name': 'resource-1'},
{'display_name': 'resource-1'}
]
}
session_response = mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources))
mocked_resource = self._mocked_switching_profile(
session_response=session_response)
self.assertEqual(resp_resources['results'],
mocked_resource.find_by_display_name('resource-1'))
def test_list_all_profiles(self):
mocked_resource = self._mocked_switching_profile()
mocked_resource.list()
test_client.assert_json_call(
'get', mocked_resource,
'https://1.2.3.4/api/v1/switching-profiles/'
'?include_system_owned=True',
data=None)
class LogicalPortTestCase(nsxlib_testcase.NsxClientTestCase):
def _mocked_lport(self, session_response=None):
return self.mocked_resource(
resources.LogicalPort, session_response=session_response)
def _get_profile_dicts(self, fake_port):
fake_profile_dicts = []
for profile_id in fake_port['switching_profile_ids']:
fake_profile_dicts.append({'resource_type': profile_id['key'],
'id': profile_id['value']})
return fake_profile_dicts
def _get_pktcls_bindings(self):
fake_pkt_classifiers = []
fake_binding_repr = []
for i in range(0, 3):
ip = "9.10.11.%s" % i
mac = "00:0c:29:35:4a:%sc" % i
fake_pkt_classifiers.append(resources.PacketAddressClassifier(
ip, mac, None))
fake_binding_repr.append({
'ip_address': ip,
'mac_address': mac
})
return fake_pkt_classifiers, fake_binding_repr
def test_create_logical_port(self):
"""Test creating a port
returns the correct response and 200 status
"""
fake_port = test_constants.FAKE_PORT.copy()
profile_dicts = self._get_profile_dicts(fake_port)
pkt_classifiers, binding_repr = self._get_pktcls_bindings()
fake_port['address_bindings'] = binding_repr
mocked_resource = self._mocked_lport()
switch_profile = resources.SwitchingProfile
mocked_resource.create(
fake_port['logical_switch_id'],
fake_port['attachment']['id'],
address_bindings=pkt_classifiers,
switch_profile_ids=switch_profile.build_switch_profile_ids(
mock.Mock(), *profile_dicts))
resp_body = {
'logical_switch_id': fake_port['logical_switch_id'],
'switching_profile_ids': fake_port['switching_profile_ids'],
'attachment': {
'attachment_type': 'VIF',
'id': fake_port['attachment']['id']
},
'admin_state': 'UP',
'address_bindings': fake_port['address_bindings']
}
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/logical-ports',
data=jsonutils.dumps(resp_body, sort_keys=True))
def test_create_logical_port_with_attachtype_cif(self):
"""Test creating a port returns the correct response and 200 status
"""
fake_port = test_constants.FAKE_CONTAINER_PORT.copy()
profile_dicts = self._get_profile_dicts(fake_port)
pkt_classifiers, binding_repr = self._get_pktcls_bindings()
fake_port['address_bindings'] = binding_repr
mocked_resource = self._mocked_lport()
switch_profile = resources.SwitchingProfile
fake_port_ctx = fake_port['attachment']['context']
fake_container_host_vif_id = fake_port_ctx['container_host_vif_id']
mocked_resource.create(
fake_port['logical_switch_id'],
fake_port['attachment']['id'],
parent_vif_id=fake_container_host_vif_id,
parent_tag=fake_port_ctx['vlan_tag'],
address_bindings=pkt_classifiers,
switch_profile_ids=switch_profile.build_switch_profile_ids(
mock.Mock(), *profile_dicts))
resp_body = {
'logical_switch_id': fake_port['logical_switch_id'],
'switching_profile_ids': fake_port['switching_profile_ids'],
'attachment': {
'attachment_type': 'CIF',
'id': fake_port['attachment']['id'],
'context': {
'vlan_tag': fake_port_ctx['vlan_tag'],
'container_host_vif_id': fake_container_host_vif_id,
'resource_type': 'CifAttachmentContext'
}
},
'admin_state': 'UP',
'address_bindings': fake_port['address_bindings']
}
test_client.assert_json_call(
'post', mocked_resource,
'https://1.2.3.4/api/v1/logical-ports',
data=jsonutils.dumps(resp_body, sort_keys=True))
def test_create_logical_port_admin_down(self):
"""Test creating port with admin_state down"""
fake_port = test_constants.FAKE_PORT
fake_port['admin_state'] = "DOWN"
mocked_resource = self._mocked_lport(
session_response=mocks.MockRequestsResponse(
200, jsonutils.dumps(fake_port)))
result = mocked_resource.create(
test_constants.FAKE_PORT['logical_switch_id'],
test_constants.FAKE_PORT['attachment']['id'],
tags={}, admin_state=False)
self.assertEqual(fake_port, result)
def test_delete_logical_port(self):
"""Test deleting port"""
mocked_resource = self._mocked_lport()
uuid = test_constants.FAKE_PORT['id']
mocked_resource.delete(uuid)
test_client.assert_json_call(
'delete', mocked_resource,
'https://1.2.3.4/api/v1/logical-ports/%s?detach=true' % uuid)
def test_clear_port_bindings(self):
fake_port = copy.copy(test_constants.FAKE_PORT)
fake_port['address_bindings'] = ['a', 'b']
mocked_resource = self._mocked_lport()
def get_fake_port(*args):
return fake_port
mocked_resource.get = get_fake_port
mocked_resource.update(
fake_port['id'], fake_port['id'], address_bindings=[])
fake_port['address_bindings'] = []
test_client.assert_json_call(
'put', mocked_resource,
'https://1.2.3.4/api/v1/logical-ports/%s' % fake_port['id'],
data=jsonutils.dumps(fake_port, sort_keys=True))
class LogicalRouterTestCase(nsxlib_testcase.NsxClientTestCase):
def _mocked_lrouter(self, session_response=None):
return self.mocked_resource(
resources.LogicalRouter, session_response=session_response)
def test_create_logical_router(self):
"""Test creating a router returns the correct response and 201 status
"""
fake_router = test_constants.FAKE_ROUTER.copy()
router = self._mocked_lrouter()
tier0_router = True
router.create(fake_router['display_name'], None, None, tier0_router)
data = {
'display_name': fake_router['display_name'],
'router_type': 'TIER0' if tier0_router else 'TIER1',
'tags': None
}
test_client.assert_json_call(
'post', router,
'https://1.2.3.4/api/v1/logical-routers',
data=jsonutils.dumps(data, sort_keys=True))
def test_delete_logical_router(self):
"""Test deleting router"""
router = self._mocked_lrouter()
uuid = test_constants.FAKE_ROUTER['id']
router.delete(uuid)
test_client.assert_json_call(
'delete', router,
'https://1.2.3.4/api/v1/logical-routers/%s' % uuid)
class LogicalRouterPortTestCase(nsxlib_testcase.NsxClientTestCase):
def _mocked_lrport(self, session_response=None):
return self.mocked_resource(
resources.LogicalRouterPort, session_response=session_response)
def test_create_logical_router_port(self):
"""Test creating a router port
returns the correct response and 201 status
"""
fake_router_port = test_constants.FAKE_ROUTER_PORT.copy()
lrport = self._mocked_lrport()
lrport.create(fake_router_port['logical_router_id'],
fake_router_port['display_name'],
None,
fake_router_port['resource_type'],
None, None, None)
data = {
'display_name': fake_router_port['display_name'],
'logical_router_id': fake_router_port['logical_router_id'],
'resource_type': fake_router_port['resource_type'],
'tags': []
}
test_client.assert_json_call(
'post', lrport,
'https://1.2.3.4/api/v1/logical-router-ports',
data=jsonutils.dumps(data, sort_keys=True))
def test_logical_router_port_max_attempts(self):
"""
Test a router port api has the configured retries
"""
lrport = self._mocked_lrport()
self.assertEqual(nsxlib_testcase.NSX_MAX_ATTEMPTS,
lrport._client.max_attempts)
def test_delete_logical_router_port(self):
"""Test deleting router port"""
lrport = self._mocked_lrport()
uuid = test_constants.FAKE_ROUTER_PORT['id']
lrport.delete(uuid)
test_client.assert_json_call(
'delete', lrport,
'https://1.2.3.4/api/v1/logical-router-ports/%s' % uuid)
def test_get_logical_router_port_by_router_id(self):
"""Test getting a router port by router id"""
fake_router_port = test_constants.FAKE_ROUTER_PORT.copy()
resp_resources = {'results': [fake_router_port]}
lrport = self._mocked_lrport(
session_response=mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources)))
router_id = fake_router_port['logical_router_id']
result = lrport.get_by_router_id(router_id)
self.assertEqual(fake_router_port, result[0])
test_client.assert_json_call(
'get', lrport,
'https://1.2.3.4/api/v1/logical-router-ports/?'
'logical_router_id=%s' % router_id)
def test_get_logical_router_port_by_switch_id(self):
"""Test getting a router port by switch id"""
fake_router_port = test_constants.FAKE_ROUTER_PORT.copy()
resp_resources = {
'result_count': 1,
'results': [fake_router_port]
}
lrport = self._mocked_lrport(
session_response=mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources)))
switch_id = test_constants.FAKE_SWITCH_UUID
lrport.get_by_lswitch_id(switch_id)
test_client.assert_json_call(
'get', lrport,
'https://1.2.3.4/api/v1/logical-router-ports/?'
'logical_switch_id=%s' % switch_id)

View File

@ -0,0 +1,83 @@
# Copyright (c) 2015 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import mock
from oslo_log import log
from vmware_nsxlib.tests.unit.v3 import mocks as nsx_v3_mocks
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.v3 import nsx_constants
LOG = log.getLogger(__name__)
class NsxLibSwitchTestCase(nsxlib_testcase.NsxClientTestCase):
_tz_id = "8f602f97-ee3e-46b0-9d9f-358955f03608"
def _create_body(self, admin_state=nsx_constants.ADMIN_STATE_UP,
vlan_id=None):
body = {
"transport_zone_id": NsxLibSwitchTestCase._tz_id,
"replication_mode": "MTEP",
"display_name": "fake_name",
"tags": [],
"admin_state": admin_state
}
if vlan_id:
body['vlan'] = vlan_id
return body
def test_create_logical_switch(self):
"""Test creating a switch returns the correct response and 200 status
"""
with mock.patch.object(self.nsxlib.client, 'create') as create:
self.nsxlib.logical_switch.create(
nsx_v3_mocks.FAKE_NAME, NsxLibSwitchTestCase._tz_id, [])
create.assert_called_with('logical-switches', self._create_body())
def test_create_logical_switch_admin_down(self):
"""Test creating switch with admin_state down"""
with mock.patch.object(self.nsxlib.client, 'create') as create:
self.nsxlib.logical_switch.create(
nsx_v3_mocks.FAKE_NAME, NsxLibSwitchTestCase._tz_id,
[], admin_state=False)
create.assert_called_with(
'logical-switches',
self._create_body(
admin_state=nsx_constants.ADMIN_STATE_DOWN))
def test_create_logical_switch_vlan(self):
"""Test creating switch with provider:network_type VLAN"""
with mock.patch.object(self.nsxlib.client, 'create') as create:
self.nsxlib.logical_switch.create(
nsx_v3_mocks.FAKE_NAME, NsxLibSwitchTestCase._tz_id,
[], vlan_id='123')
create.assert_called_with(
'logical-switches',
self._create_body(vlan_id='123'))
def test_delete_logical_switch(self):
"""Test deleting switch"""
with mock.patch.object(self.nsxlib.client, 'delete') as delete:
fake_switch = nsx_v3_mocks.make_fake_switch()
self.nsxlib.logical_switch.delete(fake_switch['id'])
delete.assert_called_with(
'logical-switches/%s'
'?detach=true&cascade=true' % fake_switch['id'])

View File

@ -0,0 +1,204 @@
# Copyright (c) 2015 OpenStack Foundation.
#
# 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.
from neutron_lib import exceptions as n_exc
from vmware_nsxlib.tests.unit.v3 import nsxlib_testcase
from vmware_nsxlib.v3 import utils
class TestNsxV3Utils(nsxlib_testcase.NsxClientTestCase):
def test_build_v3_tags_payload(self):
result = self.nsxlib.build_v3_tags_payload(
{'id': 'fake_id',
'tenant_id': 'fake_tenant_id'},
resource_type='os-net-id',
project_name='fake_tenant_name')
expected = [{'scope': 'os-net-id', 'tag': 'fake_id'},
{'scope': 'os-project-id', 'tag': 'fake_tenant_id'},
{'scope': 'os-project-name', 'tag': 'fake_tenant_name'},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(expected, result)
def test_build_v3_tags_payload_internal(self):
result = self.nsxlib.build_v3_tags_payload(
{'id': 'fake_id',
'tenant_id': 'fake_tenant_id'},
resource_type='os-net-id',
project_name=None)
expected = [{'scope': 'os-net-id', 'tag': 'fake_id'},
{'scope': 'os-project-id', 'tag': 'fake_tenant_id'},
{'scope': 'os-project-name',
'tag': nsxlib_testcase.PLUGIN_TAG},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(expected, result)
def test_build_v3_tags_payload_invalid_length(self):
self.assertRaises(n_exc.InvalidInput,
self.nsxlib.build_v3_tags_payload,
{'id': 'fake_id',
'tenant_id': 'fake_tenant_id'},
resource_type='os-longer-maldini-rocks-id',
project_name='fake')
def test_build_v3_api_version_tag(self):
result = self.nsxlib.build_v3_api_version_tag()
expected = [{'scope': nsxlib_testcase.PLUGIN_SCOPE,
'tag': nsxlib_testcase.PLUGIN_TAG},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(expected, result)
def test_is_internal_resource(self):
project_tag = self.nsxlib.build_v3_tags_payload(
{'id': 'fake_id',
'tenant_id': 'fake_tenant_id'},
resource_type='os-net-id',
project_name=None)
internal_tag = self.nsxlib.build_v3_api_version_tag()
expect_false = self.nsxlib.is_internal_resource({'tags': project_tag})
self.assertFalse(expect_false)
expect_true = self.nsxlib.is_internal_resource({'tags': internal_tag})
self.assertTrue(expect_true)
def test_get_name_and_uuid(self):
uuid = 'afc40f8a-4967-477e-a17a-9d560d1786c7'
suffix = '_afc40...786c7'
expected = 'maldini%s' % suffix
short_name = utils.get_name_and_uuid('maldini', uuid)
self.assertEqual(expected, short_name)
name = 'X' * 255
expected = '%s%s' % ('X' * (80 - len(suffix)), suffix)
short_name = utils.get_name_and_uuid(name, uuid)
self.assertEqual(expected, short_name)
def test_build_v3_tags_max_length_payload(self):
result = self.nsxlib.build_v3_tags_payload(
{'id': 'X' * 255,
'tenant_id': 'X' * 255},
resource_type='os-net-id',
project_name='X' * 255)
expected = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'X' * 40},
{'scope': 'os-project-name', 'tag': 'X' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(expected, result)
def test_add_v3_tag(self):
result = utils.add_v3_tag([], 'fake-scope', 'fake-tag')
expected = [{'scope': 'fake-scope', 'tag': 'fake-tag'}]
self.assertEqual(expected, result)
def test_add_v3_tag_max_length_payload(self):
result = utils.add_v3_tag([], 'fake-scope', 'X' * 255)
expected = [{'scope': 'fake-scope', 'tag': 'X' * 40}]
self.assertEqual(expected, result)
def test_add_v3_tag_invalid_scope_length(self):
self.assertRaises(n_exc.InvalidInput,
utils.add_v3_tag,
[],
'fake-scope-name-is-far-too-long',
'fake-tag')
def test_update_v3_tags_addition(self):
tags = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
resources = [{'scope': 'os-instance-uuid',
'tag': 'A' * 40}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER},
{'scope': 'os-instance-uuid',
'tag': 'A' * 40}]
self.assertEqual(sorted(expected, key=lambda x: x.get('tag')),
sorted(tags, key=lambda x: x.get('tag')))
def test_update_v3_tags_removal(self):
tags = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
resources = [{'scope': 'os-net-id',
'tag': ''}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(sorted(expected, key=lambda x: x.get('tag')),
sorted(tags, key=lambda x: x.get('tag')))
def test_update_v3_tags_update(self):
tags = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
resources = [{'scope': 'os-project-id',
'tag': 'A' * 40}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'A' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': nsxlib_testcase.PLUGIN_VER}]
self.assertEqual(sorted(expected, key=lambda x: x.get('tag')),
sorted(tags, key=lambda x: x.get('tag')))
def test_update_v3_tags_repetitive_scopes(self):
tags = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG1'},
{'scope': 'os-security-group', 'tag': 'SG2'}]
tags_update = [{'scope': 'os-security-group', 'tag': 'SG3'},
{'scope': 'os-security-group', 'tag': 'SG4'}]
tags = utils.update_v3_tags(tags, tags_update)
expected = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG3'},
{'scope': 'os-security-group', 'tag': 'SG4'}]
self.assertEqual(sorted(expected, key=lambda x: x.get('tag')),
sorted(tags, key=lambda x: x.get('tag')))
def test_update_v3_tags_repetitive_scopes_remove(self):
tags = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG1'},
{'scope': 'os-security-group', 'tag': 'SG2'}]
tags_update = [{'scope': 'os-security-group', 'tag': None}]
tags = utils.update_v3_tags(tags, tags_update)
expected = [{'scope': 'os-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40}]
self.assertEqual(sorted(expected, key=lambda x: x.get('tag')),
sorted(tags, key=lambda x: x.get('tag')))

View File

@ -0,0 +1,437 @@
# Copyright 2016 OpenStack Foundation
# 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.
from oslo_log import log
from vmware_nsxlib._i18n import _, _LW
from vmware_nsxlib.v3 import client
from vmware_nsxlib.v3 import cluster
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import native_dhcp
from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3 import security
from vmware_nsxlib.v3 import utils
LOG = log.getLogger(__name__)
class NsxLib(object):
def __init__(self, nsxlib_config):
self.nsxlib_config = nsxlib_config
# create the Cluster
self.cluster = cluster.NSXClusteredAPI(nsxlib_config)
# create the Client
self.client = client.NSX3Client(
self.cluster,
max_attempts=nsxlib_config.max_attempts)
# init the api object
self.general_apis = utils.NsxLibApiBase(
self.client, nsxlib_config)
self.port_mirror = NsxLibPortMirror(
self.client, nsxlib_config)
self.bridge_endpoint = NsxLibBridgeEndpoint(
self.client, nsxlib_config)
self.logical_switch = NsxLibLogicalSwitch(
self.client, nsxlib_config)
self.logical_router = NsxLibLogicalRouter(
self.client, nsxlib_config)
self.qos_switching_profile = NsxLibQosSwitchingProfile(
self.client, nsxlib_config)
self.edge_cluster = NsxLibEdgeCluster(
self.client, nsxlib_config)
self.bridge_cluster = NsxLibBridgeCluster(
self.client, nsxlib_config)
self.transport_zone = NsxLibTransportZone(
self.client, nsxlib_config)
self.firewall_section = security.NsxLibFirewallSection(
self.client, nsxlib_config)
self.ns_group = security.NsxLibNsGroup(
self.client, nsxlib_config, self.firewall_section)
self.native_dhcp = native_dhcp.NsxLibNativeDhcp(
self.client, nsxlib_config)
super(NsxLib, self).__init__()
def get_version(self):
node = self.client.get("node")
version = node.get('node_version')
return version
def build_v3_api_version_tag(self):
return self.general_apis.build_v3_api_version_tag()
def is_internal_resource(self, nsx_resource):
return self.general_apis.is_internal_resource(nsx_resource)
def build_v3_tags_payload(self, resource, resource_type, project_name):
return self.general_apis.build_v3_tags_payload(
resource, resource_type, project_name)
def reinitialize_cluster(self, resource, event, trigger, **kwargs):
self.cluster._reinit_cluster()
class NsxLibPortMirror(utils.NsxLibApiBase):
def create_session(self, source_ports, dest_ports, direction,
description, name, tags):
"""Create a PortMirror Session on the backend.
:param source_ports: List of UUIDs of the ports whose traffic is to be
mirrored.
:param dest_ports: List of UUIDs of the ports where the mirrored
traffic is to be sent.
:param direction: String representing the direction of traffic to be
mirrored. [INGRESS, EGRESS, BIDIRECTIONAL]
:param description: String representing the description of the session.
:param name: String representing the name of the session.
:param tags: nsx backend specific tags.
"""
resource = 'mirror-sessions'
body = {'direction': direction,
'tags': tags,
'display_name': name,
'description': description,
'mirror_sources': source_ports,
'mirror_destination': dest_ports}
return self.client.create(resource, body)
def delete_session(self, mirror_session_id):
"""Delete a PortMirror session on the backend.
:param mirror_session_id: string representing the UUID of the port
mirror session to be deleted.
"""
resource = 'mirror-sessions/%s' % mirror_session_id
self.client.delete(resource)
class NsxLibBridgeEndpoint(utils.NsxLibApiBase):
def create(self, device_name, seg_id, tags):
"""Create a bridge endpoint on the backend.
Create a bridge endpoint resource on a bridge cluster for the L2
gateway network connection.
:param device_name: device_name actually refers to the bridge cluster's
UUID.
:param seg_id: integer representing the VLAN segmentation ID.
:param tags: nsx backend specific tags.
"""
resource = 'bridge-endpoints'
body = {'bridge_cluster_id': device_name,
'tags': tags,
'vlan': seg_id}
return self.client.create(resource, body)
def delete(self, bridge_endpoint_id):
"""Delete a bridge endpoint on the backend.
:param bridge_endpoint_id: string representing the UUID of the bridge
endpoint to be deleted.
"""
resource = 'bridge-endpoints/%s' % bridge_endpoint_id
self.client.delete(resource)
class NsxLibLogicalSwitch(utils.NsxLibApiBase):
def create(self, display_name, transport_zone_id, tags,
replication_mode=nsx_constants.MTEP,
admin_state=True, vlan_id=None):
# TODO(salv-orlando): Validate Replication mode and admin_state
# NOTE: These checks might be moved to the API client library if one
# that performs such checks in the client is available
resource = 'logical-switches'
body = {'transport_zone_id': transport_zone_id,
'replication_mode': replication_mode,
'display_name': display_name,
'tags': tags}
if admin_state:
body['admin_state'] = nsx_constants.ADMIN_STATE_UP
else:
body['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
if vlan_id:
body['vlan'] = vlan_id
return self.client.create(resource, body)
def delete(self, lswitch_id):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def _do_delete():
resource = ('logical-switches/%s?detach=true&cascade=true' %
lswitch_id)
self.client.delete(resource)
_do_delete()
def get(self, logical_switch_id):
resource = "logical-switches/%s" % logical_switch_id
return self.client.get(resource)
def update(self, lswitch_id, name=None, admin_state=None, tags=None):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def _do_update():
resource = "logical-switches/%s" % lswitch_id
lswitch = self.get(lswitch_id)
if name is not None:
lswitch['display_name'] = name
if admin_state is not None:
if admin_state:
lswitch['admin_state'] = nsx_constants.ADMIN_STATE_UP
else:
lswitch['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
if tags is not None:
lswitch['tags'] = tags
return self.client.update(resource, lswitch)
return _do_update()
class NsxLibQosSwitchingProfile(utils.NsxLibApiBase):
def _build_args(self, tags, name=None, description=None):
body = {"resource_type": "QosSwitchingProfile",
"tags": tags}
return self._update_args(
body, name=name, description=description)
def _update_args(self, body, name=None, description=None):
if name:
body["display_name"] = name
if description:
body["description"] = description
return body
def _enable_shaping_in_args(self, body, burst_size=None,
peak_bandwidth=None, average_bandwidth=None):
for shaper in body["shaper_configuration"]:
# We currently supports only shaping of Egress traffic
if shaper["resource_type"] == "EgressRateShaper":
shaper["enabled"] = True
if burst_size:
shaper["burst_size_bytes"] = burst_size
if peak_bandwidth:
shaper["peak_bandwidth_mbps"] = peak_bandwidth
if average_bandwidth:
shaper["average_bandwidth_mbps"] = average_bandwidth
break
return body
def _disable_shaping_in_args(self, body):
for shaper in body["shaper_configuration"]:
# We currently supports only shaping of Egress traffic
if shaper["resource_type"] == "EgressRateShaper":
shaper["enabled"] = False
shaper["burst_size_bytes"] = 0
shaper["peak_bandwidth_mbps"] = 0
shaper["average_bandwidth_mbps"] = 0
break
return body
def _update_dscp_in_args(self, body, qos_marking, dscp):
body["dscp"] = {}
body["dscp"]["mode"] = qos_marking.upper()
if dscp:
body["dscp"]["priority"] = dscp
return body
def create(self, tags, name=None, description=None):
resource = 'switching-profiles'
body = self._build_args(tags, name, description)
return self.client.create(resource, body)
def update(self, profile_id, tags, name=None, description=None):
resource = 'switching-profiles/%s' % profile_id
# get the current configuration
body = self.get(profile_id)
# update the relevant fields
body = self._update_args(body, name, description)
return self._update_resource_with_retry(resource, body)
def update_shaping(self, profile_id,
shaping_enabled=False,
burst_size=None,
peak_bandwidth=None,
average_bandwidth=None,
qos_marking=None, dscp=None):
resource = 'switching-profiles/%s' % profile_id
# get the current configuration
body = self.get(profile_id)
# update the relevant fields
if shaping_enabled:
body = self._enable_shaping_in_args(
body, burst_size=burst_size,
peak_bandwidth=peak_bandwidth,
average_bandwidth=average_bandwidth)
else:
body = self._disable_shaping_in_args(body)
body = self._update_dscp_in_args(body, qos_marking, dscp)
return self._update_resource_with_retry(resource, body)
def get(self, profile_id):
resource = 'switching-profiles/%s' % profile_id
return self.client.get(resource)
def delete(self, profile_id):
resource = 'switching-profiles/%s' % profile_id
self.client.delete(resource)
class NsxLibLogicalRouter(utils.NsxLibApiBase):
def _delete_resource_by_values(self, resource,
skip_not_found=True, **kwargs):
resources_get = self.client.get(resource)
matched_num = 0
for res in resources_get['results']:
if utils.dict_match(kwargs, res):
LOG.debug("Deleting %s from resource %s", res, resource)
delete_resource = resource + "/" + str(res['id'])
self.client.delete(delete_resource)
matched_num = matched_num + 1
if matched_num == 0:
if skip_not_found:
LOG.warning(_LW("No resource in %(res)s matched for values: "
"%(values)s"), {'res': resource,
'values': kwargs})
else:
err_msg = (_("No resource in %(res)s matched for values: "
"%(values)s") % {'res': resource,
'values': kwargs})
raise exceptions.ResourceNotFound(
manager=self.cluster.nsx_api_managers,
operation=err_msg)
elif matched_num > 1:
LOG.warning(_LW("%(num)s resources in %(res)s matched for values: "
"%(values)s"), {'num': matched_num,
'res': resource,
'values': kwargs})
def add_nat_rule(self, logical_router_id, action, translated_network,
source_net=None, dest_net=None,
enabled=True, rule_priority=None):
resource = 'logical-routers/%s/nat/rules' % logical_router_id
body = {'action': action,
'enabled': enabled,
'translated_network': translated_network}
if source_net:
body['match_source_network'] = source_net
if dest_net:
body['match_destination_network'] = dest_net
if rule_priority:
body['rule_priority'] = rule_priority
return self.client.create(resource, body)
def add_static_route(self, logical_router_id, dest_cidr, nexthop):
resource = ('logical-routers/%s/routing/static-routes' %
logical_router_id)
body = {}
if dest_cidr:
body['network'] = dest_cidr
if nexthop:
body['next_hops'] = [{"ip_address": nexthop}]
return self.client.create(resource, body)
def delete_static_route(self, logical_router_id, static_route_id):
resource = 'logical-routers/%s/routing/static-routes/%s' % (
logical_router_id, static_route_id)
self.client.delete(resource)
def delete_static_route_by_values(self, logical_router_id,
dest_cidr=None, nexthop=None):
resource = ('logical-routers/%s/routing/static-routes' %
logical_router_id)
kwargs = {}
if dest_cidr:
kwargs['network'] = dest_cidr
if nexthop:
kwargs['next_hops'] = [{"ip_address": nexthop}]
return self._delete_resource_by_values(resource, **kwargs)
def delete_nat_rule(self, logical_router_id, nat_rule_id):
resource = 'logical-routers/%s/nat/rules/%s' % (logical_router_id,
nat_rule_id)
self.client.delete(resource)
def delete_nat_rule_by_values(self, logical_router_id, **kwargs):
resource = 'logical-routers/%s/nat/rules' % logical_router_id
return self._delete_resource_by_values(resource, **kwargs)
def update_advertisement(self, logical_router_id, **kwargs):
resource = ('logical-routers/%s/routing/advertisement' %
logical_router_id)
return self._update_resource_with_retry(resource, kwargs)
def get_id_by_name_or_id(self, name_or_id):
"""Get a logical router by it's display name or uuid
Return the logical router data, or raise an exception if not found or
not unique
"""
return self._get_resource_by_name_or_id(name_or_id,
'logical-routers')
class NsxLibEdgeCluster(utils.NsxLibApiBase):
def get(self, edge_cluster_uuid):
resource = "edge-clusters/%s" % edge_cluster_uuid
return self.client.get(resource)
class NsxLibTransportZone(utils.NsxLibApiBase):
def get_id_by_name_or_id(self, name_or_id):
"""Get a transport zone by it's display name or uuid
Return the transport zone data, or raise an exception if not found or
not unique
"""
return self._get_resource_by_name_or_id(name_or_id,
'transport-zones')
class NsxLibBridgeCluster(utils.NsxLibApiBase):
def get_id_by_name_or_id(self, name_or_id):
"""Get a bridge cluster by it's display name or uuid
Return the bridge cluster data, or raise an exception if not found or
not unique
"""
return self._get_resource_by_name_or_id(name_or_id,
'bridge-clusters')

208
vmware_nsxlib/v3/client.py Normal file
View File

@ -0,0 +1,208 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import requests
import six.moves.urllib.parse as urlparse
from oslo_log import log
from oslo_serialization import jsonutils
from vmware_nsxlib._i18n import _, _LW
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import utils
LOG = log.getLogger(__name__)
ERRORS = {requests.codes.NOT_FOUND: exceptions.ResourceNotFound,
requests.codes.PRECONDITION_FAILED: exceptions.StaleRevision}
DEFAULT_ERROR = exceptions.ManagerError
class RESTClient(object):
_VERB_RESP_CODES = {
'get': [requests.codes.ok],
'post': [requests.codes.created, requests.codes.ok],
'put': [requests.codes.ok],
'delete': [requests.codes.ok]
}
def __init__(self, connection, url_prefix=None,
default_headers=None,
max_attempts=utils.DEFAULT_MAX_ATTEMPTS):
self._conn = connection
self._url_prefix = url_prefix or ""
self._default_headers = default_headers or {}
self.max_attempts = max_attempts
def new_client_for(self, *uri_segments):
uri = self._build_url('/'.join(uri_segments))
return self.__class__(
self._conn,
url_prefix=uri,
default_headers=self._default_headers,
max_attempts=self.max_attempts)
def list(self, headers=None):
return self.url_list('')
def get(self, uuid, headers=None):
return self.url_get(uuid, headers=headers)
def delete(self, uuid, headers=None):
return self.url_delete(uuid, headers=headers)
def update(self, uuid, body=None, headers=None):
return self.url_put(uuid, body, headers=headers)
def create(self, resource='', body=None, headers=None):
return self.url_post(resource, body, headers=headers)
def url_list(self, url, headers=None):
return self.url_get(url, headers=headers)
def url_get(self, url, headers=None):
return self._rest_call(url, method='GET', headers=headers)
def url_delete(self, url, headers=None):
return self._rest_call(url, method='DELETE', headers=headers)
def url_put(self, url, body, headers=None):
return self._rest_call(url, method='PUT', body=body, headers=headers)
def url_post(self, url, body, headers=None):
return self._rest_call(url, method='POST', body=body, headers=headers)
def _raise_error(self, status_code, operation, result_msg):
error = ERRORS.get(status_code, DEFAULT_ERROR)
raise error(manager='', operation=operation, details=result_msg)
def _validate_result(self, result, expected, operation):
if result.status_code not in expected:
result_msg = result.json() if result.content else ''
LOG.warning(_LW("The HTTP request returned error code "
"%(result)d, whereas %(expected)s response "
"codes were expected. Response body %(body)s"),
{'result': result.status_code,
'expected': '/'.join([str(code)
for code in expected]),
'body': result_msg})
if isinstance(result_msg, dict) and 'error_message' in result_msg:
related_errors = [error['error_message'] for error in
result_msg.get('related_errors', [])]
result_msg = result_msg['error_message']
if related_errors:
result_msg += " relatedErrors: %s" % ' '.join(
related_errors)
self._raise_error(result.status_code, operation, result_msg)
@classmethod
def merge_headers(cls, *headers):
merged = {}
for header in headers:
if header:
merged.update(header)
return merged
def _build_url(self, uri):
prefix = urlparse.urlparse(self._url_prefix)
uri = ("/%s/%s" % (prefix.path, uri)).replace('//', '/').strip('/')
if prefix.netloc:
uri = "%s/%s" % (prefix.netloc, uri)
if prefix.scheme:
uri = "%s://%s" % (prefix.scheme, uri)
return uri
def _rest_call(self, url, method='GET', body=None, headers=None):
request_headers = headers.copy() if headers else {}
request_headers.update(self._default_headers)
request_url = self._build_url(url)
do_request = getattr(self._conn, method.lower())
LOG.debug("REST call: %s %s\nHeaders: %s\nBody: %s",
method, request_url, request_headers, body)
result = do_request(
request_url,
data=body,
headers=request_headers)
LOG.debug("REST call: %s %s\nResponse: %s",
method, request_url, result.json() if result.content else '')
self._validate_result(
result, RESTClient._VERB_RESP_CODES[method.lower()],
_("%(verb)s %(url)s") % {'verb': method, 'url': request_url})
return result
class JSONRESTClient(RESTClient):
_DEFAULT_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def __init__(self, connection, url_prefix=None,
default_headers=None,
max_attempts=utils.DEFAULT_MAX_ATTEMPTS):
super(JSONRESTClient, self).__init__(
connection,
url_prefix=url_prefix,
default_headers=RESTClient.merge_headers(
JSONRESTClient._DEFAULT_HEADERS, default_headers),
max_attempts=max_attempts)
def _rest_call(self, *args, **kwargs):
if kwargs.get('body') is not None:
kwargs['body'] = jsonutils.dumps(kwargs['body'], sort_keys=True)
result = super(JSONRESTClient, self)._rest_call(*args, **kwargs)
return result.json() if result.content else result
class NSX3Client(JSONRESTClient):
_NSX_V1_API_PREFIX = 'api/v1/'
def __init__(self, connection, url_prefix=None,
default_headers=None,
nsx_api_managers=None,
max_attempts=utils.DEFAULT_MAX_ATTEMPTS):
self.nsx_api_managers = nsx_api_managers or []
url_prefix = url_prefix or NSX3Client._NSX_V1_API_PREFIX
if url_prefix and NSX3Client._NSX_V1_API_PREFIX not in url_prefix:
if url_prefix.startswith('http'):
url_prefix += '/' + NSX3Client._NSX_V1_API_PREFIX
else:
url_prefix = "%s/%s" % (NSX3Client._NSX_V1_API_PREFIX,
url_prefix or '')
self.max_attempts = max_attempts
super(NSX3Client, self).__init__(
connection, url_prefix=url_prefix,
default_headers=default_headers,
max_attempts=max_attempts)
def _raise_error(self, status_code, operation, result_msg):
"""Override the Rest client errors to add the manager IPs"""
error = ERRORS.get(status_code, DEFAULT_ERROR)
raise error(manager=self.nsx_api_managers,
operation=operation,
details=result_msg)

493
vmware_nsxlib/v3/cluster.py Normal file
View File

@ -0,0 +1,493 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import abc
import contextlib
import copy
import datetime
import eventlet
import itertools
import logging
import requests
import six
import six.moves.urllib.parse as urlparse
from eventlet import greenpool
from eventlet import pools
from oslo_log import log
from oslo_service import loopingcall
from requests import adapters
from requests import exceptions as requests_exceptions
from vmware_nsxlib._i18n import _, _LI, _LW
from vmware_nsxlib.v3 import client as nsx_client
from vmware_nsxlib.v3 import exceptions
LOG = log.getLogger(__name__)
# disable warning message for each HTTP retry
logging.getLogger(
"requests.packages.urllib3.connectionpool").setLevel(logging.ERROR)
@six.add_metaclass(abc.ABCMeta)
class AbstractHTTPProvider(object):
"""Interface for providers of HTTP connections which
are responsible for creating and validating connections
for their underlying HTTP support.
"""
@property
def default_scheme(self):
return 'https'
@abc.abstractproperty
def provider_id(self):
"""A unique string name for this provider."""
pass
@abc.abstractmethod
def validate_connection(self, cluster_api, endpoint, conn):
"""Validate the said connection for the given endpoint and cluster.
"""
pass
@abc.abstractmethod
def new_connection(self, cluster_api, provider):
"""Create a new http connection for the said cluster and
cluster provider. The actual connection should duck type
requests.Session http methods (get(), put(), etc.).
"""
pass
@abc.abstractmethod
def is_connection_exception(self, exception):
"""Determine if the given exception is related to connection
failure. Return True if it's a connection exception and
False otherwise.
"""
class TimeoutSession(requests.Session):
"""Extends requests.Session to support timeout
at the session level.
"""
def __init__(self, timeout, read_timeout):
self.timeout = timeout
self.read_timeout = read_timeout
super(TimeoutSession, self).__init__()
# wrapper timeouts at the session level
# see: https://goo.gl/xNk7aM
def request(self, *args, **kwargs):
if 'timeout' not in kwargs:
kwargs['timeout'] = (self.timeout, self.read_timeout)
return super(TimeoutSession, self).request(*args, **kwargs)
class NSXRequestsHTTPProvider(AbstractHTTPProvider):
"""Concrete implementation of AbstractHTTPProvider
using requests.Session() as the underlying connection.
"""
@property
def provider_id(self):
return "%s-%s" % (requests.__title__, requests.__version__)
def validate_connection(self, cluster_api, endpoint, conn):
client = nsx_client.NSX3Client(conn, url_prefix=endpoint.provider.url)
zones = client.get('transport-zones')
if not zones or zones['result_count'] <= 0:
msg = _("No transport zones found "
"for '%s'") % endpoint.provider.url
LOG.warning(msg)
raise exceptions.ResourceNotFound(
manager=endpoint.provider.url, operation=msg)
def new_connection(self, cluster_api, provider):
config = cluster_api.nsxlib_config
session = TimeoutSession(config.http_timeout,
config.http_read_timeout)
session.auth = (provider.username, provider.password)
# NSX v3 doesn't use redirects
session.max_redirects = 0
session.verify = not config.insecure
if session.verify and provider.ca_file:
# verify using the said ca bundle path
session.verify = provider.ca_file
# we are pooling with eventlet in the cluster class
adapter = adapters.HTTPAdapter(
pool_connections=1, pool_maxsize=1,
max_retries=config.retries,
pool_block=False)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def is_connection_exception(self, exception):
return isinstance(exception, requests_exceptions.ConnectionError)
class ClusterHealth(object):
"""Indicator of overall cluster health with respect
to the connectivity of the clusters managed endpoints.
"""
# all endpoints are UP
GREEN = 'GREEN'
# at least 1 endpoint is UP, but 1 or more are DOWN
ORANGE = 'ORANGE'
# all endpoints are DOWN
RED = 'RED'
class EndpointState(object):
"""Tracks the connectivity state for a said endpoint.
"""
# no UP or DOWN state recorded yet
INITIALIZED = 'INITIALIZED'
# endpoint has been validate and is good
UP = 'UP'
# endpoint can't be reached or validated
DOWN = 'DOWN'
class Provider(object):
"""Data holder for a provider which has a unique id
a connection URL, and the credential details.
"""
def __init__(self, provider_id, provider_url, username, password, ca_file):
self.id = provider_id
self.url = provider_url
self.username = username
self.password = password
self.ca_file = ca_file
def __str__(self):
return str(self.url)
class Endpoint(object):
"""A single NSX manager endpoint (host) which includes
related information such as the endpoint's provider,
state, etc.. A pool is used to hold connections to the
endpoint which are doled out when proxying HTTP methods
to the underlying connections.
"""
def __init__(self, provider, pool):
self.provider = provider
self.pool = pool
self._state = EndpointState.INITIALIZED
self._last_updated = datetime.datetime.now()
@property
def last_updated(self):
return self._last_updated
@property
def state(self):
return self._state
def set_state(self, state):
if self.state != state:
LOG.info(_LI("Endpoint '%(ep)s' changing from state"
" '%(old)s' to '%(new)s'"),
{'ep': self.provider,
'old': self.state,
'new': state})
old_state = self._state
self._state = state
self._last_updated = datetime.datetime.now()
return old_state
def __str__(self):
return "[%s] %s" % (self.state, self.provider)
class EndpointConnection(object):
"""Simple data holder which contains an endpoint and
a connection for that endpoint.
"""
def __init__(self, endpoint, connection):
self.endpoint = endpoint
self.connection = connection
class ClusteredAPI(object):
"""Duck types the major HTTP based methods of a
requests.Session such as get(), put(), post(), etc.
and transparently proxies those calls to one of
its managed NSX manager endpoints.
"""
_HTTP_VERBS = ['get', 'delete', 'head', 'put', 'post', 'patch', 'create']
def __init__(self, providers,
http_provider,
min_conns_per_pool=1,
max_conns_per_pool=500,
keepalive_interval=33):
self._http_provider = http_provider
self._keepalive_interval = keepalive_interval
def _init_cluster(*args, **kwargs):
self._init_endpoints(providers,
min_conns_per_pool, max_conns_per_pool)
_init_cluster()
# keep this internal method for reinitialize upon fork
# for api workers to ensure each process has its own keepalive
# loops + state
self._reinit_cluster = _init_cluster
def _init_endpoints(self, providers,
min_conns_per_pool, max_conns_per_pool):
LOG.debug("Initializing API endpoints")
def _create_conn(p):
def _conn():
# called when a pool needs to create a new connection
return self._http_provider.new_connection(self, p)
return _conn
self._endpoints = {}
for provider in providers:
pool = pools.Pool(
min_size=min_conns_per_pool,
max_size=max_conns_per_pool,
order_as_stack=True,
create=_create_conn(provider))
endpoint = Endpoint(provider, pool)
self._endpoints[provider.id] = endpoint
# service requests using round robin
self._endpoint_schedule = itertools.cycle(self._endpoints.values())
# duck type to proxy http invocations
for method in ClusteredAPI._HTTP_VERBS:
setattr(self, method, self._proxy_stub(method))
conns = greenpool.GreenPool()
for endpoint in self._endpoints.values():
conns.spawn(self._validate, endpoint)
eventlet.sleep(0)
while conns.running():
if (self.health == ClusterHealth.GREEN
or self.health == ClusterHealth.ORANGE):
# only wait for 1 or more endpoints to reduce init time
break
eventlet.sleep(0.5)
for endpoint in self._endpoints.values():
# dynamic loop for each endpoint to ensure connectivity
loop = loopingcall.DynamicLoopingCall(
self._endpoint_keepalive, endpoint)
loop.start(initial_delay=self._keepalive_interval,
periodic_interval_max=self._keepalive_interval,
stop_on_exception=False)
LOG.debug("Done initializing API endpoint(s). "
"API cluster health: %s", self.health)
def _endpoint_keepalive(self, endpoint):
delta = datetime.datetime.now() - endpoint.last_updated
if delta.seconds >= self._keepalive_interval:
# TODO(boden): backoff on validation failure
self._validate(endpoint)
return self._keepalive_interval
return self._keepalive_interval - delta.seconds
@property
def providers(self):
return [ep.provider for ep in self._endpoints.values()]
@property
def endpoints(self):
return copy.copy(self._endpoints)
@property
def http_provider(self):
return self._http_provider
@property
def health(self):
down = 0
up = 0
for endpoint in self._endpoints.values():
if endpoint.state != EndpointState.UP:
down += 1
else:
up += 1
if down == len(self._endpoints):
return ClusterHealth.RED
return (ClusterHealth.GREEN
if up == len(self._endpoints)
else ClusterHealth.ORANGE)
def _validate(self, endpoint):
try:
with endpoint.pool.item() as conn:
self._http_provider.validate_connection(self, endpoint, conn)
endpoint.set_state(EndpointState.UP)
except Exception as e:
endpoint.set_state(EndpointState.DOWN)
LOG.warning(_LW("Failed to validate API cluster endpoint "
"'%(ep)s' due to: %(err)s"),
{'ep': endpoint, 'err': e})
def _select_endpoint(self):
# check for UP state until exhausting all endpoints
seen, total = 0, len(self._endpoints.values())
while seen < total:
endpoint = next(self._endpoint_schedule)
if endpoint.state == EndpointState.UP:
return endpoint
seen += 1
def endpoint_for_connection(self, conn):
# check all endpoint pools
for endpoint in self._endpoints.values():
if (conn in endpoint.pool.channel.queue or
conn in endpoint.pool.free_items):
return endpoint
@property
def cluster_id(self):
return ','.join([str(ep.provider.url)
for ep in self._endpoints.values()])
@contextlib.contextmanager
def connection(self):
with self.endpoint_connection() as conn_data:
yield conn_data.connection
@contextlib.contextmanager
def endpoint_connection(self):
endpoint = self._select_endpoint()
if not endpoint:
LOG.debug("All endpoints down for: %s" %
[str(ep) for ep in self._endpoints.values()])
# all endpoints are DOWN and will have their next
# state updated as per _endpoint_keepalive()
raise exceptions.ServiceClusterUnavailable(
cluster_id=self.cluster_id)
if endpoint.pool.free() == 0:
LOG.info(_LI("API endpoint %(ep)s at connection "
"capacity %(max)s and has %(waiting)s waiting"),
{'ep': endpoint,
'max': endpoint.pool.max_size,
'waiting': endpoint.pool.waiting()})
# pool.item() will wait if pool has 0 free
with endpoint.pool.item() as conn:
yield EndpointConnection(endpoint, conn)
def _proxy_stub(self, proxy_for):
def _call_proxy(url, *args, **kwargs):
return self._proxy(proxy_for, url, *args, **kwargs)
return _call_proxy
def _proxy(self, proxy_for, uri, *args, **kwargs):
# proxy http request call to an avail endpoint
with self.endpoint_connection() as conn_data:
conn = conn_data.connection
endpoint = conn_data.endpoint
# http conn must support requests style interface
do_request = getattr(conn, proxy_for)
if not uri.startswith('/'):
uri = "/%s" % uri
url = "%s%s" % (endpoint.provider.url, uri)
try:
LOG.debug("API cluster proxy %s %s to %s",
proxy_for.upper(), uri, url)
# call the actual connection method to do the
# http request/response over the wire
response = do_request(url, *args, **kwargs)
endpoint.set_state(EndpointState.UP)
return response
except Exception as e:
LOG.warning(_LW("Request failed due to: %s"), e)
if not self._http_provider.is_connection_exception(e):
# only trap and retry connection errors
raise e
endpoint.set_state(EndpointState.DOWN)
LOG.debug("Connection to %s failed, checking additional "
"endpoints" % url)
# retry until exhausting endpoints
return self._proxy(proxy_for, uri, *args, **kwargs)
class NSXClusteredAPI(ClusteredAPI):
"""Extends ClusteredAPI to get conf values and setup the
NSX v3 cluster.
"""
def __init__(self, nsxlib_config):
self.nsxlib_config = nsxlib_config
self._http_provider = (nsxlib_config.http_provider or
NSXRequestsHTTPProvider())
super(NSXClusteredAPI, self).__init__(
self._build_conf_providers(),
self._http_provider,
max_conns_per_pool=self.nsxlib_config.concurrent_connections,
keepalive_interval=self.nsxlib_config.conn_idle_timeout)
LOG.debug("Created NSX clustered API with '%s' "
"provider", self._http_provider.provider_id)
def _build_conf_providers(self):
def _schemed_url(uri):
uri = uri.strip('/')
return urlparse.urlparse(
uri if uri.startswith('http') else
"%s://%s" % (self._http_provider.default_scheme, uri))
conf_urls = self.nsxlib_config.nsx_api_managers[:]
urls = []
providers = []
provider_index = -1
for conf_url in conf_urls:
provider_index += 1
conf_url = _schemed_url(conf_url)
if conf_url in urls:
LOG.warning(_LW("'%s' already defined in configuration file. "
"Skipping."), urlparse.urlunparse(conf_url))
continue
urls.append(conf_url)
providers.append(
Provider(
conf_url.netloc,
urlparse.urlunparse(conf_url),
self.nsxlib_config.username(provider_index),
self.nsxlib_config.password(provider_index),
self.nsxlib_config.ca_file(provider_index)))
return providers

123
vmware_nsxlib/v3/config.py Normal file
View File

@ -0,0 +1,123 @@
# Copyright 2016 VMware, Inc.
# 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.
class NsxLibConfig(object):
"""Class holding all the configuration parameters used by the nsxlib code.
:param nsx_api_managers: List of IP addresses of the NSX managers.
Each IP address should be of the form:
[<scheme>://]<ip_adress>[:<port>]
If scheme is not provided https is used.
If port is not provided port 80 is used for http
and port 443 for https.
:param username: User name for the NSX manager
:param password: Password for the NSX manager
:param insecure: If true, the NSX Manager server certificate is not
verified. If false the CA bundle specified via "ca_file"
will be used or if unsest the default system root CAs
will be used.
:param ca_file: Specify a CA bundle file to use in verifying the NSX
Manager server certificate. This option is ignored if
"insecure" is set to True. If "insecure" is set to
False and ca_file is unset, the system root CAs will
be used to verify the server certificate.
:param concurrent_connections: Maximum concurrent connections to each NSX
manager.
:param retries: Maximum number of times to retry a HTTP connection.
:param http_timeout: The time in seconds before aborting a HTTP connection
to a NSX manager.
:param http_read_timeout: The time in seconds before aborting a HTTP read
response from a NSX manager.
:param conn_idle_timeout: The amount of time in seconds to wait before
ensuring connectivity to the NSX manager if no
manager connection has been used.
:param http_provider: HTTPProvider object, or None.
:param max_attempts: Maximum number of times to retry API requests upon
stale revision errors.
:param plugin_scope: The default scope for the v3 api-version tag
:param plugin_tag: The value for the v3 api-version tag
:param plugin_ver: The version of the plugin used as the 'os-api-version'
tag value in the v3 api-version tag
:param dns_nameservers: List of nameservers to configure for the DHCP
binding entries. These will be used if there are
no nameservers defined on the subnet.
:param dns_domain: Domain to use for building the hostnames.
:param dhcp_profile_uuid: The UUID of the NSX DHCP Profile that will be
used to enable native DHCP service.
"""
def __init__(self,
nsx_api_managers=None,
username=None,
password=None,
insecure=True,
ca_file=None,
concurrent_connections=10,
retries=3,
http_timeout=10,
http_read_timeout=180,
conn_idle_timeout=10,
http_provider=None,
max_attempts=10,
plugin_scope=None,
plugin_tag=None,
plugin_ver=None,
dns_nameservers=None,
dns_domain='openstacklocal',
dhcp_profile_uuid=None):
self.nsx_api_managers = nsx_api_managers
self._username = username
self._password = password
self._ca_file = ca_file
self.insecure = insecure
self.concurrent_connections = concurrent_connections
self.retries = retries
self.http_timeout = http_timeout
self.http_read_timeout = http_read_timeout
self.conn_idle_timeout = conn_idle_timeout
self.http_provider = http_provider
self.max_attempts = max_attempts
self.plugin_scope = plugin_scope
self.plugin_tag = plugin_tag
self.plugin_ver = plugin_ver
self.dns_nameservers = dns_nameservers or []
self.dns_domain = dns_domain
self.dhcp_profile_uuid = dhcp_profile_uuid
def _attribute_by_index(self, scalar_or_list, index):
if isinstance(scalar_or_list, list):
if not len(scalar_or_list):
return None
if len(scalar_or_list) > index:
return scalar_or_list[index]
# if not long enough - use the first one as default
return scalar_or_list[0]
# this is a scalar
return scalar_or_list
def username(self, index):
return self._attribute_by_index(self._username, index)
def password(self, index):
return self._attribute_by_index(self._password, index)
def ca_file(self, index):
return self._attribute_by_index(self._ca_file, index)

View File

@ -0,0 +1,97 @@
# Copyright 2016 VMware, Inc.
# 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.
#
from oslo_utils import excutils
import six
from vmware_nsxlib._i18n import _
class NsxLibException(Exception):
"""Base NsxLib Exception.
To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd
with the keyword arguments provided to the constructor.
"""
message = _("An unknown exception occurred.")
def __init__(self, **kwargs):
try:
super(NsxLibException, self).__init__(self.message % kwargs)
self.msg = self.message % kwargs
except Exception:
with excutils.save_and_reraise_exception() as ctxt:
if not self.use_fatal_exceptions():
ctxt.reraise = False
# at least get the core message out if something happened
super(NsxLibException, self).__init__(self.message)
if six.PY2:
def __unicode__(self):
return unicode(self.msg)
def __str__(self):
return self.msg
def use_fatal_exceptions(self):
return False
class ManagerError(NsxLibException):
message = _("Unexpected error from backend manager (%(manager)s) "
"for %(operation)s %(details)s")
def __init__(self, **kwargs):
kwargs['details'] = (': %s' % kwargs['details']
if 'details' in kwargs
else '')
super(ManagerError, self).__init__(**kwargs)
self.msg = self.message % kwargs
class ResourceNotFound(ManagerError):
message = _("Resource could not be found on backend (%(manager)s) for "
"%(operation)s")
class StaleRevision(ManagerError):
pass
class ServiceClusterUnavailable(ManagerError):
message = _("Service cluster: '%(cluster_id)s' is unavailable. Please, "
"check NSX setup and/or configuration")
class NSGroupMemberNotFound(ManagerError):
message = _("Could not find NSGroup %(nsgroup_id)s member %(member_id)s "
"for removal.")
class NSGroupIsFull(ManagerError):
message = _("NSGroup %(nsgroup_id)s contains has reached its maximum "
"capacity, unable to add additional members.")
class NumberOfNsgroupCriteriaTagsReached(ManagerError):
message = _("Port can be associated with at most %(max_num)s "
"security-groups.")
class SecurityGroupMaximumCapacityReached(ManagerError):
message = _("Security Group %(sg_id)s has reached its maximum capacity, "
"no more ports can be associated with this security-group.")

View File

@ -0,0 +1,64 @@
# Copyright 2016 VMware, Inc.
# 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.
import netaddr
from neutron_lib.api import validators
from neutron_lib import constants
from vmware_nsxlib.v3 import utils
class NsxLibNativeDhcp(utils.NsxLibApiBase):
def build_server_config(self, network, subnet, port, tags):
# Prepare the configuration for a new logical DHCP server.
server_ip = "%s/%u" % (port['fixed_ips'][0]['ip_address'],
netaddr.IPNetwork(subnet['cidr']).prefixlen)
dns_nameservers = subnet['dns_nameservers']
if not dns_nameservers or not validators.is_attr_set(dns_nameservers):
dns_nameservers = self.nsxlib_config.dns_nameservers
gateway_ip = subnet['gateway_ip']
if not validators.is_attr_set(gateway_ip):
gateway_ip = None
# The following code is based on _generate_opts_per_subnet() in
# neutron/agent/linux/dhcp.py. It prepares DHCP options for a subnet.
# Add route for directly connected network.
host_routes = [{'network': subnet['cidr'], 'next_hop': '0.0.0.0'}]
# Copy routes from subnet host_routes attribute.
for hr in subnet['host_routes']:
if hr['destination'] == constants.IPv4_ANY:
if not gateway_ip:
gateway_ip = hr['nexthop']
else:
host_routes.append({'network': hr['destination'],
'next_hop': hr['nexthop']})
# If gateway_ip is defined, add default route via this gateway.
if gateway_ip:
host_routes.append({'network': constants.IPv4_ANY,
'next_hop': gateway_ip})
options = {'option121': {'static_routes': host_routes}}
name = utils.get_name_and_uuid(network['name'] or 'dhcpserver',
network['id'])
return {'name': name,
'dhcp_profile_id': self.nsxlib_config.dhcp_profile_uuid,
'server_ip': server_ip,
'dns_nameservers': dns_nameservers,
'domain_name': self.nsxlib_config.dns_domain,
'gateway_ip': gateway_ip,
'options': options,
'tags': tags}

View File

@ -0,0 +1,150 @@
# Copyright 2015 OpenStack Foundation
# 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.
import uuid
from oslo_log import log
from vmware_nsxlib._i18n import _, _LW
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import nsx_constants as consts
LOG = log.getLogger(__name__)
class NSGroupManager(object):
"""
This class assists with NSX integration for Neutron security-groups,
Each Neutron security-group is associated with NSX NSGroup object.
Some specific security policies are the same across all security-groups,
i.e - Default drop rule, DHCP. In order to bind these rules to all
NSGroups (security-groups), we create a nested NSGroup (which its members
are also of type NSGroups) to group the other NSGroups and associate it
with these rules.
In practice, one NSGroup (nested) can't contain all the other NSGroups, as
it has strict size limit. To overcome the limited space challenge, we
create several nested groups instead of just one, and we evenly distribute
NSGroups (security-groups) between them.
By using an hashing function on the NSGroup uuid we determine in which
group it should be added, and when deleting an NSGroup (security-group) we
use the same procedure to find which nested group it was added.
"""
NESTED_GROUP_NAME = 'OS Nested Group'
NESTED_GROUP_DESCRIPTION = ('OpenStack NSGroup. Do not delete.')
def __init__(self, nsxlib, size):
self.nsxlib_nsgroup = nsxlib.ns_group
self._nested_groups = self._init_nested_groups(size)
self._size = len(self._nested_groups)
@property
def size(self):
return self._size
@property
def nested_groups(self):
return self._nested_groups
def _init_nested_groups(self, requested_size):
# Construct the groups dict -
# {0: <groups-1>,.., n-1: <groups-n>}
size = requested_size
nested_groups = {
self._get_nested_group_index_from_name(nsgroup): nsgroup['id']
for nsgroup in self.nsxlib_nsgroup.list()
if self.nsxlib_nsgroup.is_internal_resource(nsgroup)}
if nested_groups:
size = max(requested_size, max(nested_groups) + 1)
if size > requested_size:
LOG.warning(_LW("Lowering the value of "
"nsx_v3:number_of_nested_groups isn't "
"supported, '%s' nested-groups will be used."),
size)
absent_groups = set(range(size)) - set(nested_groups.keys())
if absent_groups:
LOG.warning(
_LW("Found %(num_present)s Nested Groups, "
"creating %(num_absent)s more."),
{'num_present': len(nested_groups),
'num_absent': len(absent_groups)})
for i in absent_groups:
cont = self._create_nested_group(i)
nested_groups[i] = cont['id']
return nested_groups
def _get_nested_group_index_from_name(self, nested_group):
# The name format is "Nested Group <index+1>"
return int(nested_group['display_name'].split()[-1]) - 1
def _create_nested_group(self, index):
name_prefix = NSGroupManager.NESTED_GROUP_NAME
name = '%s %s' % (name_prefix, index + 1)
description = NSGroupManager.NESTED_GROUP_DESCRIPTION
tags = self.nsxlib_nsgroup.build_v3_api_version_tag()
return self.nsxlib_nsgroup.create(name, description, tags)
def _hash_uuid(self, internal_id):
return hash(uuid.UUID(internal_id))
def _suggest_nested_group(self, internal_id):
# Suggests a nested group to use, can be iterated to find alternative
# group in case that previous suggestions did not help.
index = self._hash_uuid(internal_id) % self.size
yield self.nested_groups[index]
for i in range(1, self.size):
index = (index + 1) % self.size
yield self.nested_groups[index]
def add_nsgroup(self, nsgroup_id):
for group in self._suggest_nested_group(nsgroup_id):
try:
LOG.debug("Adding NSGroup %s to nested group %s",
nsgroup_id, group)
self.nsxlib_nsgroup.add_members(
group, consts.NSGROUP, [nsgroup_id])
break
except exceptions.NSGroupIsFull:
LOG.debug("Nested group %(group_id)s is full, trying the "
"next group..", {'group_id': group})
else:
raise exceptions.ManagerError(
details=_("Reached the maximum supported amount of "
"security groups."))
def remove_nsgroup(self, nsgroup_id):
for group in self._suggest_nested_group(nsgroup_id):
try:
self.nsxlib_nsgroup.remove_member(
group, consts.NSGROUP,
nsgroup_id, verify=True)
break
except exceptions.NSGroupMemberNotFound:
LOG.warning(_LW("NSGroup %(nsgroup)s was expected to be found "
"in group %(group_id)s, but wasn't. "
"Looking in the next group.."),
{'nsgroup': nsgroup_id, 'group_id': group})
continue
else:
LOG.warning(_LW("NSGroup %s was marked for removal, but its "
"reference is missing."), nsgroup_id)

View File

@ -0,0 +1,96 @@
# Copyright 2016 VMware, Inc.
# 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.
# Admin statuses
ADMIN_STATE_UP = "UP"
ADMIN_STATE_DOWN = "DOWN"
# Replication modes
MTEP = "MTEP"
# Port attachment types
ATTACHMENT_VIF = "VIF"
ATTACHMENT_CIF = "CIF"
ATTACHMENT_LR = "LOGICALROUTER"
ATTACHMENT_DHCP = "DHCP_SERVICE"
ATTACHMENT_MDPROXY = "METADATA_PROXY"
CIF_RESOURCE_TYPE = "CifAttachmentContext"
# NSXv3 L2 Gateway constants
BRIDGE_ENDPOINT = "BRIDGEENDPOINT"
# Router type
ROUTER_TYPE_TIER0 = "TIER0"
ROUTER_TYPE_TIER1 = "TIER1"
LROUTERPORT_UPLINK = "LogicalRouterUplinkPort"
LROUTERPORT_DOWNLINK = "LogicalRouterDownLinkPort"
LROUTERPORT_LINKONTIER0 = "LogicalRouterLinkPortOnTIER0"
LROUTERPORT_LINKONTIER1 = "LogicalRouterLinkPortOnTIER1"
# NSX service type
SERVICE_DHCP = "dhcp"
# NSX-V3 Distributed Firewall constants
NSGROUP = 'NSGroup'
NSGROUP_SIMPLE_EXP = 'NSGroupSimpleExpression'
NSGROUP_TAG_EXP = 'NSGroupTagExpression'
# Firewall rule position
FW_INSERT_BEFORE = 'insert_before'
FW_INSERT_BOTTOM = 'insert_bottom'
FW_INSERT_TOP = 'insert_top'
# firewall rule actions
FW_ACTION_ALLOW = 'ALLOW'
FW_ACTION_DROP = 'DROP'
FW_ACTION_REJECT = 'REJECT'
# nsgroup members update actions
NSGROUP_ADD_MEMBERS = 'ADD_MEMBERS'
NSGROUP_REMOVE_MEMBERS = 'REMOVE_MEMBERS'
# NSServices resource types
L4_PORT_SET_NSSERVICE = 'L4PortSetNSService'
ICMP_TYPE_NSSERVICE = 'ICMPTypeNSService'
IP_PROTOCOL_NSSERVICE = 'IPProtocolNSService'
# firewall section types
FW_SECTION_LAYER3 = 'LAYER3'
TARGET_TYPE_LOGICAL_SWITCH = 'LogicalSwitch'
TARGET_TYPE_LOGICAL_PORT = 'LogicalPort'
TARGET_TYPE_IPV4ADDRESS = 'IPv4Address'
TARGET_TYPE_IPV6ADDRESS = 'IPv6Address'
# filtering operators and expressions
EQUALS = 'EQUALS'
IN = 'IN'
OUT = 'OUT'
IN_OUT = 'IN_OUT'
TCP = 'TCP'
UDP = 'UDP'
ICMPV4 = 'ICMPv4'
ICMPV6 = 'ICMPv6'
IPV4 = 'IPV4'
IPV6 = 'IPV6'
IPV4_IPV6 = 'IPV4_IPV6'
LOCAL_IP_PREFIX = 'local_ip_prefix'
LOGGING = 'logging'

View File

@ -0,0 +1,576 @@
# Copyright 2015 VMware, Inc.
# 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.
#
import abc
import collections
import six
from vmware_nsxlib._i18n import _
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3 import utils
SwitchingProfileTypeId = collections.namedtuple(
'SwitchingProfileTypeId', 'profile_type, profile_id')
PacketAddressClassifier = collections.namedtuple(
'PacketAddressClassifier', 'ip_address, mac_address, vlan')
@six.add_metaclass(abc.ABCMeta)
class AbstractRESTResource(object):
def __init__(self, rest_client, *args, **kwargs):
self._client = rest_client.new_client_for(self.uri_segment)
@abc.abstractproperty
def uri_segment(self):
pass
def list(self):
return self._client.list()
def get(self, uuid):
return self._client.get(uuid)
def delete(self, uuid):
return self._client.delete(uuid)
@abc.abstractmethod
def create(self, *args, **kwargs):
pass
@abc.abstractmethod
def update(self, uuid, *args, **kwargs):
pass
def find_by_display_name(self, display_name):
found = []
for resource in self.list()['results']:
if resource['display_name'] == display_name:
found.append(resource)
return found
class SwitchingProfileTypes(object):
IP_DISCOVERY = 'IpDiscoverySwitchingProfile'
MAC_LEARNING = 'MacManagementSwitchingProfile'
PORT_MIRRORING = 'PortMirroringSwitchingProfile'
QOS = 'QosSwitchingProfile'
SPOOF_GUARD = 'SpoofGuardSwitchingProfile'
SWITCH_SECURITY = 'SwitchSecuritySwitchingProfile'
class WhiteListAddressTypes(object):
PORT = 'LPORT_BINDINGS'
SWITCH = 'LSWITCH_BINDINGS'
class SwitchingProfile(AbstractRESTResource):
@property
def uri_segment(self):
return 'switching-profiles'
def list(self):
return self._client.url_get('?include_system_owned=True')
def create(self, profile_type, display_name=None,
description=None, **api_args):
body = {
'resource_type': profile_type,
'display_name': display_name or '',
'description': description or ''
}
body.update(api_args)
return self._client.create(body=body)
def update(self, uuid, profile_type, **api_args):
body = {
'resource_type': profile_type
}
body.update(api_args)
return self._client.update(uuid, body=body)
def create_spoofguard_profile(self, display_name,
description,
whitelist_ports=False,
whitelist_switches=False,
tags=None):
whitelist_providers = []
if whitelist_ports:
whitelist_providers.append(WhiteListAddressTypes.PORT)
if whitelist_switches:
whitelist_providers.append(WhiteListAddressTypes.SWITCH)
return self.create(SwitchingProfileTypes.SPOOF_GUARD,
display_name=display_name,
description=description,
white_list_providers=whitelist_providers,
tags=tags or [])
def create_dhcp_profile(self, display_name,
description, tags=None):
dhcp_filter = {
'client_block_enabled': True,
'server_block_enabled': False
}
rate_limits = {
'enabled': False,
'rx_broadcast': 0,
'tx_broadcast': 0,
'rx_multicast': 0,
'tx_multicast': 0
}
bpdu_filter = {
'enabled': True,
'white_list': []
}
return self.create(SwitchingProfileTypes.SWITCH_SECURITY,
display_name=display_name,
description=description,
tags=tags or [],
dhcp_filter=dhcp_filter,
rate_limits=rate_limits,
bpdu_filter=bpdu_filter,
block_non_ip_traffic=True)
def create_mac_learning_profile(self, display_name,
description, tags=None):
mac_learning = {
'enabled': True,
}
return self.create(SwitchingProfileTypes.MAC_LEARNING,
display_name=display_name,
description=description,
tags=tags or [],
mac_learning=mac_learning)
def create_port_mirror_profile(self, display_name, description,
direction, destinations, tags=None):
return self.create(SwitchingProfileTypes.PORT_MIRRORING,
display_name=display_name,
description=description,
tags=tags or [],
direction=direction,
destinations=destinations)
@classmethod
def build_switch_profile_ids(cls, client, *profiles):
ids = []
for profile in profiles:
if isinstance(profile, str):
profile = client.get(profile)
if not isinstance(profile, SwitchingProfileTypeId):
profile = SwitchingProfileTypeId(
profile.get('key', profile.get('resource_type')),
profile.get('value', profile.get('id')))
ids.append(profile)
return ids
class LogicalPort(AbstractRESTResource):
@property
def uri_segment(self):
return 'logical-ports'
def _build_body_attrs(
self, display_name=None,
admin_state=True, tags=None,
address_bindings=None,
switch_profile_ids=None,
attachment=None):
tags = tags or []
address_bindings = address_bindings or []
switch_profile_ids = switch_profile_ids or []
body = {}
if tags:
body['tags'] = tags
if display_name is not None:
body['display_name'] = display_name
if admin_state is not None:
if admin_state:
body['admin_state'] = nsx_constants.ADMIN_STATE_UP
else:
body['admin_state'] = nsx_constants.ADMIN_STATE_DOWN
if address_bindings:
bindings = []
for binding in address_bindings:
address_classifier = {
'ip_address': binding.ip_address,
'mac_address': binding.mac_address
}
if binding.vlan is not None:
address_classifier['vlan'] = int(binding.vlan)
bindings.append(address_classifier)
body['address_bindings'] = bindings
elif address_bindings == []:
# explicitly clear out address bindings
body['address_bindings'] = []
if switch_profile_ids:
profiles = []
for profile in switch_profile_ids:
profiles.append({
'value': profile.profile_id,
'key': profile.profile_type
})
body['switching_profile_ids'] = profiles
# Note that attachment could be None, meaning reset it.
if attachment is not False:
body['attachment'] = attachment
return body
def _prepare_attachment(self, vif_uuid, parent_vif_id, parent_tag,
address_bindings, attachment_type):
if attachment_type and vif_uuid:
attachment = {'attachment_type': attachment_type,
'id': vif_uuid}
if parent_vif_id:
context = {'vlan_tag': parent_tag,
'container_host_vif_id': parent_vif_id,
'resource_type': nsx_constants.CIF_RESOURCE_TYPE}
attachment['context'] = context
return attachment
elif attachment_type is None or vif_uuid is None:
return None # reset attachment
else:
return False # no attachment change
def create(self, lswitch_id, vif_uuid, tags=None,
attachment_type=nsx_constants.ATTACHMENT_VIF,
admin_state=True, name=None, address_bindings=None,
parent_vif_id=None, parent_tag=None,
switch_profile_ids=None):
tags = tags or []
body = {'logical_switch_id': lswitch_id}
# NOTE(arosen): If parent_vif_id is specified we need to use
# CIF attachment type.
if parent_vif_id:
attachment_type = nsx_constants.ATTACHMENT_CIF
attachment = self._prepare_attachment(vif_uuid, parent_vif_id,
parent_tag, address_bindings,
attachment_type)
body.update(self._build_body_attrs(
display_name=name,
admin_state=admin_state, tags=tags,
address_bindings=address_bindings,
switch_profile_ids=switch_profile_ids,
attachment=attachment))
return self._client.create(body=body)
def delete(self, lport_id):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_delete():
return self._client.url_delete('%s?detach=true' % lport_id)
return _do_delete()
def update(self, lport_id, vif_uuid,
name=None, admin_state=None,
address_bindings=None, switch_profile_ids=None,
tags_update=None,
attachment_type=nsx_constants.ATTACHMENT_VIF,
parent_vif_id=None, parent_tag=None):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def do_update():
lport = self.get(lport_id)
tags = lport.get('tags', [])
if tags_update:
tags = utils.update_v3_tags(tags, tags_update)
attachment = self._prepare_attachment(vif_uuid, parent_vif_id,
parent_tag, address_bindings,
attachment_type)
lport.update(self._build_body_attrs(
display_name=name,
admin_state=admin_state, tags=tags,
address_bindings=address_bindings,
switch_profile_ids=switch_profile_ids,
attachment=attachment))
# If revision_id of the payload that we send is older than what
# NSX has, we will get a 412: Precondition Failed.
# In that case we need to re-fetch, patch the response and send
# it again with the new revision_id
return self._client.update(lport_id, body=lport)
return do_update()
class LogicalRouter(AbstractRESTResource):
@property
def uri_segment(self):
return 'logical-routers'
def create(self, display_name, tags, edge_cluster_uuid=None, tier_0=False):
# TODO(salv-orlando): If possible do not manage edge clusters
# in the main plugin logic.
router_type = (nsx_constants.ROUTER_TYPE_TIER0 if tier_0 else
nsx_constants.ROUTER_TYPE_TIER1)
body = {'display_name': display_name,
'router_type': router_type,
'tags': tags}
if edge_cluster_uuid:
body['edge_cluster_id'] = edge_cluster_uuid
return self._client.create(body=body)
def delete(self, lrouter_id):
return self._client.url_delete(lrouter_id)
def update(self, lrouter_id, *args, **kwargs):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_update():
lrouter = self.get(lrouter_id)
for k in kwargs:
lrouter[k] = kwargs[k]
# If revision_id of the payload that we send is older than what
# NSX has, we will get a 412: Precondition Failed.
# In that case we need to re-fetch, patch the response and send
# it again with the new revision_id
return self._client.update(lrouter_id, body=lrouter)
return _do_update()
class LogicalRouterPort(AbstractRESTResource):
@property
def uri_segment(self):
return 'logical-router-ports'
def create(self, logical_router_id,
display_name,
tags,
resource_type,
logical_port_id,
address_groups,
edge_cluster_member_index=None):
body = {'display_name': display_name,
'resource_type': resource_type,
'logical_router_id': logical_router_id,
'tags': tags or []}
if address_groups:
body['subnets'] = address_groups
if resource_type in [nsx_constants.LROUTERPORT_UPLINK,
nsx_constants.LROUTERPORT_DOWNLINK]:
body['linked_logical_switch_port_id'] = {
'target_id': logical_port_id}
elif resource_type == nsx_constants.LROUTERPORT_LINKONTIER1:
body['linked_logical_router_port_id'] = {
'target_id': logical_port_id}
elif logical_port_id:
body['linked_logical_router_port_id'] = logical_port_id
if edge_cluster_member_index:
body['edge_cluster_member_index'] = edge_cluster_member_index
return self._client.create(body=body)
def update(self, logical_port_id, **kwargs):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_update():
logical_router_port = self.get(logical_port_id)
for k in kwargs:
logical_router_port[k] = kwargs[k]
# If revision_id of the payload that we send is older than what
# NSX has, we will get a 412: Precondition Failed.
# In that case we need to re-fetch, patch the response and send
# it again with the new revision_id
return self._client.update(logical_port_id,
body=logical_router_port)
return _do_update()
def delete(self, logical_port_id):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_delete():
return self._client.url_delete(logical_port_id)
return _do_delete()
def get_by_lswitch_id(self, logical_switch_id):
resource = '?logical_switch_id=%s' % logical_switch_id
router_ports = self._client.url_get(resource)
result_count = int(router_ports.get('result_count', "0"))
if result_count >= 2:
raise exceptions.ManagerError(
details=_("Can't support more than one logical router ports "
"on same logical switch %s ") % logical_switch_id)
elif result_count == 1:
return router_ports['results'][0]
else:
err_msg = (_("Logical router link port not found on logical "
"switch %s") % logical_switch_id)
raise exceptions.ResourceNotFound(
manager=self._client.nsx_api_managers,
operation=err_msg)
def update_by_lswitch_id(self, logical_router_id, ls_id, **payload):
port = self.get_by_lswitch_id(ls_id)
return self.update(port['id'], **payload)
def delete_by_lswitch_id(self, ls_id):
port = self.get_by_lswitch_id(ls_id)
self.delete(port['id'])
def get_by_router_id(self, logical_router_id):
resource = '?logical_router_id=%s' % logical_router_id
logical_router_ports = self._client.url_get(resource)
return logical_router_ports['results']
def get_tier1_link_port(self, logical_router_id):
logical_router_ports = self.get_by_router_id(logical_router_id)
for port in logical_router_ports:
if port['resource_type'] == nsx_constants.LROUTERPORT_LINKONTIER1:
return port
raise exceptions.ResourceNotFound(
manager=self._client.nsx_api_managers,
operation="get router link port")
class MetaDataProxy(AbstractRESTResource):
@property
def uri_segment(self):
return 'md-proxies'
def create(self, *args, **kwargs):
pass
def update(self, uuid, *args, **kwargs):
pass
class DhcpProfile(AbstractRESTResource):
@property
def uri_segment(self):
return 'dhcp/server-profiles'
def create(self, *args, **kwargs):
pass
def update(self, uuid, *args, **kwargs):
pass
class LogicalDhcpServer(AbstractRESTResource):
@property
def uri_segment(self):
return 'dhcp/servers'
def _construct_server(self, body, dhcp_profile_id=None, server_ip=None,
name=None, dns_nameservers=None, domain_name=None,
gateway_ip=False, options=None, tags=None):
if name:
body['display_name'] = name
if dhcp_profile_id:
body['dhcp_profile_id'] = dhcp_profile_id
if server_ip:
body['ipv4_dhcp_server']['dhcp_server_ip'] = server_ip
if dns_nameservers is not None:
# Note that [] is valid for dns_nameservers, means deleting it.
body['ipv4_dhcp_server']['dns_nameservers'] = dns_nameservers
if domain_name:
body['ipv4_dhcp_server']['domain_name'] = domain_name
if gateway_ip is not False:
# Note that None is valid for gateway_ip, means deleting it.
body['ipv4_dhcp_server']['gateway_ip'] = gateway_ip
if options:
body['ipv4_dhcp_server']['options'] = options
if tags:
body['tags'] = tags
def create(self, dhcp_profile_id, server_ip, name=None,
dns_nameservers=None, domain_name=None, gateway_ip=False,
options=None, tags=None):
body = {'ipv4_dhcp_server': {}}
self._construct_server(body, dhcp_profile_id, server_ip, name,
dns_nameservers, domain_name, gateway_ip,
options, tags)
return self._client.create(body=body)
def update(self, uuid, dhcp_profile_id=None, server_ip=None, name=None,
dns_nameservers=None, domain_name=None, gateway_ip=False,
options=None, tags=None):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_update():
body = self._client.get(uuid)
self._construct_server(body, dhcp_profile_id, server_ip, name,
dns_nameservers, domain_name, gateway_ip,
options, tags)
return self._client.update(uuid, body=body)
return _do_update()
def create_binding(self, server_uuid, mac, ip, hostname=None,
lease_time=None, options=None):
body = {'mac_address': mac, 'ip_address': ip}
if hostname:
body['host_name'] = hostname
if lease_time:
body['lease_time'] = lease_time
if options:
body['options'] = options
url = "%s/static-bindings" % server_uuid
return self._client.url_post(url, body)
def get_binding(self, server_uuid, binding_uuid):
url = "%s/static-bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_get(url)
def update_binding(self, server_uuid, binding_uuid, **kwargs):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self._client.max_attempts)
def _do_update():
body = self.get_binding(server_uuid, binding_uuid)
body.update(kwargs)
url = "%s/static-bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_put(url, body)
return _do_update()
def delete_binding(self, server_uuid, binding_uuid):
url = "%s/static-bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_delete(url)

194
vmware_nsxlib/v3/router.py Normal file
View File

@ -0,0 +1,194 @@
# Copyright 2015 VMware, Inc.
# 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.
"""
NSX-V3 Plugin router module
"""
import copy
from neutron_lib import exceptions as n_exc
from oslo_log import log
from vmware_nsxlib._i18n import _, _LW
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3 import utils
LOG = log.getLogger(__name__)
MIN_EDGE_NODE_NUM = 1
TIER0_ROUTER_LINK_PORT_NAME = "TIER0-RouterLinkPort"
TIER1_ROUTER_LINK_PORT_NAME = "TIER1-RouterLinkPort"
ROUTER_INTF_PORT_NAME = "Tier1-RouterDownLinkPort"
FIP_NAT_PRI = 900
GW_NAT_PRI = 1000
class RouterLib(object):
def __init__(self, router_client, router_port_client, nsxlib):
self._router_client = router_client
self._router_port_client = router_port_client
self.nsxlib = nsxlib
def validate_tier0(self, tier0_groups_dict, tier0_uuid):
err_msg = None
try:
lrouter = self._router_client.get(tier0_uuid)
except exceptions.ResourceNotFound:
err_msg = (_("Tier0 router %s not found at the backend. Either a "
"valid UUID must be specified or a default tier0 "
"router UUID must be configured in nsx.ini") %
tier0_uuid)
else:
edge_cluster_uuid = lrouter.get('edge_cluster_id')
if not edge_cluster_uuid:
err_msg = _("Failed to get edge cluster uuid from tier0 "
"router %s at the backend") % lrouter
else:
edge_cluster = self.nsxlib.edge_cluster.get(edge_cluster_uuid)
member_index_list = [member['member_index']
for member in edge_cluster['members']]
if len(member_index_list) < MIN_EDGE_NODE_NUM:
err_msg = _("%(act_num)s edge members found in "
"edge_cluster %(cluster_id)s, however we "
"require at least %(exp_num)s edge nodes "
"in edge cluster for use.") % {
'act_num': len(member_index_list),
'exp_num': MIN_EDGE_NODE_NUM,
'cluster_id': edge_cluster_uuid}
if err_msg:
raise n_exc.InvalidInput(error_message=err_msg)
else:
tier0_groups_dict[tier0_uuid] = {
'edge_cluster_uuid': edge_cluster_uuid,
'member_index_list': member_index_list}
def add_router_link_port(self, tier1_uuid, tier0_uuid, tags):
# Create Tier0 logical router link port
t0_tags = copy.copy(tags)
t0_tags = utils.add_v3_tag(t0_tags, 'os-tier0-uuid', tier0_uuid)
tier0_link_port = self._router_port_client.create(
tier0_uuid, display_name=TIER0_ROUTER_LINK_PORT_NAME, tags=t0_tags,
resource_type=nsx_constants.LROUTERPORT_LINKONTIER0,
logical_port_id=None,
address_groups=None)
linked_logical_port_id = tier0_link_port['id']
# Create Tier1 logical router link port
t1_tags = copy.copy(tags)
t1_tags = utils.add_v3_tag(t1_tags, 'os-tier1-uuid', tier1_uuid)
self._router_port_client.create(
tier1_uuid, display_name=TIER1_ROUTER_LINK_PORT_NAME, tags=t1_tags,
resource_type=nsx_constants.LROUTERPORT_LINKONTIER1,
logical_port_id=linked_logical_port_id,
address_groups=None)
def remove_router_link_port(self, tier1_uuid, tier0_uuid):
try:
tier1_link_port = (
self._router_port_client.get_tier1_link_port(tier1_uuid))
except exceptions.ResourceNotFound:
LOG.warning(_LW("Logical router link port for tier1 router: %s "
"not found at the backend"), tier1_uuid)
return
tier1_link_port_id = tier1_link_port['id']
tier0_link_port_id = (
tier1_link_port['linked_logical_router_port_id'].get('target_id'))
self._router_port_client.delete(tier1_link_port_id)
self._router_port_client.delete(tier0_link_port_id)
def update_advertisement(self, logical_router_id,
advertise_route_nat,
advertise_route_connected,
advertise_route_static=False,
enabled=True):
return self.nsxlib.logical_router.update_advertisement(
logical_router_id,
advertise_nat_routes=advertise_route_nat,
advertise_nsx_connected_routes=advertise_route_connected,
advertise_static_routes=advertise_route_static,
enabled=enabled)
def delete_gw_snat_rule(self, logical_router_id, gw_ip):
return self.nsxlib.logical_router.delete_nat_rule_by_values(
logical_router_id,
translated_network=gw_ip)
def add_gw_snat_rule(self, logical_router_id, gw_ip):
return self.nsxlib.logical_router.add_nat_rule(
logical_router_id, action="SNAT",
translated_network=gw_ip,
rule_priority=GW_NAT_PRI)
def update_router_edge_cluster(self, nsx_router_id, edge_cluster_uuid):
return self._router_client.update(nsx_router_id,
edge_cluster_id=edge_cluster_uuid)
def create_logical_router_intf_port_by_ls_id(self, logical_router_id,
display_name,
tags,
ls_id,
logical_switch_port_id,
address_groups):
try:
port = self._router_port_client.get_by_lswitch_id(ls_id)
except exceptions.ResourceNotFound:
return self._router_port_client.create(
logical_router_id,
display_name,
tags,
nsx_constants.LROUTERPORT_DOWNLINK,
logical_switch_port_id,
address_groups)
else:
return self._router_port_client.update(
port['id'], subnets=address_groups)
def add_fip_nat_rules(self, logical_router_id, ext_ip, int_ip):
self.nsxlib.logical_router.add_nat_rule(
logical_router_id, action="SNAT",
translated_network=ext_ip,
source_net=int_ip,
rule_priority=FIP_NAT_PRI)
self.nsxlib.logical_router.add_nat_rule(
logical_router_id, action="DNAT",
translated_network=int_ip,
dest_net=ext_ip,
rule_priority=FIP_NAT_PRI)
def delete_fip_nat_rules(self, logical_router_id, ext_ip, int_ip):
self.nsxlib.logical_router.delete_nat_rule_by_values(
logical_router_id,
action="SNAT",
translated_network=ext_ip,
match_source_network=int_ip)
self.nsxlib.logical_router.delete_nat_rule_by_values(
logical_router_id,
action="DNAT",
translated_network=int_ip,
match_destination_network=ext_ip)
def add_static_routes(self, nsx_router_id, route):
return self.nsxlib.logical_router.add_static_route(
nsx_router_id,
route['destination'],
route['nexthop'])
def delete_static_routes(self, nsx_router_id, route):
return self.nsxlib.logical_router.delete_static_route_by_values(
nsx_router_id, dest_cidr=route['destination'],
nexthop=route['nexthop'])

View File

@ -0,0 +1,529 @@
# Copyright 2015 OpenStack Foundation
# 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.
"""
NSX-V3 Plugin security & Distributed Firewall integration module
"""
from neutron_lib import constants
from oslo_log import log
from oslo_utils import excutils
from vmware_nsxlib._i18n import _LE, _LW
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import nsx_constants as consts
from vmware_nsxlib.v3 import utils
LOG = log.getLogger(__name__)
PORT_SG_SCOPE = 'os-security-group'
MAX_NSGROUPS_CRITERIA_TAGS = 10
class NsxLibNsGroup(utils.NsxLibApiBase):
def __init__(self, client, max_attempts, firewall_section_handler):
self.firewall_section = firewall_section_handler
super(NsxLibNsGroup, self).__init__(client, max_attempts)
def update_on_backend(self, context, security_group,
nsgroup_id, section_id,
log_sg_allowed_traffic):
name = self.get_name(security_group)
description = security_group['description']
logging = (log_sg_allowed_traffic or
security_group[consts.LOGGING])
rules = self.firewall_section._process_rules_logging_for_update(
section_id, logging)
self.update(nsgroup_id, name, description)
self.firewall_section.update(section_id, name, description,
rules=rules)
def get_name(self, security_group):
# NOTE(roeyc): We add the security-group id to the NSGroup name,
# for usability purposes.
return '%(name)s - %(id)s' % security_group
def get_lport_tags(self, secgroups):
if len(secgroups) > MAX_NSGROUPS_CRITERIA_TAGS:
raise exceptions.NumberOfNsgroupCriteriaTagsReached(
max_num=MAX_NSGROUPS_CRITERIA_TAGS)
tags = []
for sg in secgroups:
tags = utils.add_v3_tag(tags, PORT_SG_SCOPE, sg)
if not tags:
# This port shouldn't be associated with any security-group
tags = [{'scope': PORT_SG_SCOPE, 'tag': None}]
return tags
def update_lport(self, context, lport_id, original, updated):
added = set(updated) - set(original)
removed = set(original) - set(updated)
for nsgroup_id in added:
try:
self.add_members(
nsgroup_id, consts.TARGET_TYPE_LOGICAL_PORT,
[lport_id])
except exceptions.NSGroupIsFull:
for nsgroup_id in added:
# NOTE(roeyc): If the port was not added to the nsgroup
# yet, then this request will silently fail.
self.remove_member(
nsgroup_id, consts.TARGET_TYPE_LOGICAL_PORT,
lport_id)
raise exceptions.SecurityGroupMaximumCapacityReached(
sg_id=nsgroup_id)
except exceptions.ResourceNotFound:
with excutils.save_and_reraise_exception():
LOG.error(_LE("NSGroup %s doesn't exists"), nsgroup_id)
for nsgroup_id in removed:
self.remove_member(
nsgroup_id, consts.TARGET_TYPE_LOGICAL_PORT, lport_id)
def init_default_section(self, name, description, nested_groups,
log_sg_blocked_traffic):
fw_sections = self.list_sections()
for section in fw_sections:
if section['display_name'] == name:
break
else:
tags = utils.build_v3_api_version_tag()
section = self.create_empty_section(
name, description, nested_groups, tags)
block_rule = self.get_firewall_rule_dict(
'Block All', action=consts.FW_ACTION_DROP,
logged=log_sg_blocked_traffic)
# TODO(roeyc): Add additional rules to allow IPV6 NDP.
dhcp_client = self.get_nsservice(
consts.L4_PORT_SET_NSSERVICE,
l4_protocol=consts.UDP,
source_ports=[67],
destination_ports=[68])
dhcp_client_rule_in = self.get_firewall_rule_dict(
'DHCP Reply',
direction=consts.IN,
service=dhcp_client)
dhcp_server = (
self.get_nsservice(consts.L4_PORT_SET_NSSERVICE,
l4_protocol=consts.UDP,
source_ports=[68],
destination_ports=[67]))
dhcp_client_rule_out = self.get_firewall_rule_dict(
'DHCP Request',
direction=consts.OUT,
service=dhcp_server)
self.update_section(section['id'],
name, section['description'],
applied_tos=nested_groups,
rules=[dhcp_client_rule_out,
dhcp_client_rule_in,
block_rule])
return section['id']
def get_nsservice(self, resource_type, **properties):
service = {'resource_type': resource_type}
service.update(properties)
return {'service': service}
def get_port_tag_expression(self, scope, tag):
return {'resource_type': consts.NSGROUP_TAG_EXP,
'target_type': consts.TARGET_TYPE_LOGICAL_PORT,
'scope': scope,
'tag': tag}
def create(self, display_name, description, tags,
membership_criteria=None):
body = {'display_name': display_name,
'description': description,
'tags': tags,
'members': []}
if membership_criteria:
body.update({'membership_criteria': [membership_criteria]})
return self.client.create('ns-groups', body)
def list(self):
return self.client.get(
'ns-groups?populate_references=false').get('results', [])
def update(self, nsgroup_id, display_name=None, description=None,
membership_criteria=None, members=None):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def _do_update():
nsgroup = self.read(nsgroup_id)
if display_name is not None:
nsgroup['display_name'] = display_name
if description is not None:
nsgroup['description'] = description
if members is not None:
nsgroup['members'] = members
if membership_criteria is not None:
nsgroup['membership_criteria'] = [membership_criteria]
return self.client.update(
'ns-groups/%s' % nsgroup_id, nsgroup)
return _do_update()
def get_member_expression(self, target_type, target_id):
return {
'resource_type': consts.NSGROUP_SIMPLE_EXP,
'target_property': 'id',
'target_type': target_type,
'op': consts.EQUALS,
'value': target_id}
def _update_with_members(self, nsgroup_id, members, action):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def _do_update():
members_update = 'ns-groups/%s?action=%s' % (nsgroup_id, action)
return self.client.create(members_update, members)
return _do_update()
def add_members(self, nsgroup_id, target_type, target_ids):
members = []
for target_id in target_ids:
member_expr = self.get_member_expression(
target_type, target_id)
members.append(member_expr)
members = {'members': members}
try:
return self._update_with_members(
nsgroup_id, members, consts.NSGROUP_ADD_MEMBERS)
except (exceptions.StaleRevision, exceptions.ResourceNotFound):
raise
except exceptions.ManagerError:
# REVISIT(roeyc): A ManagerError might have been raised for a
# different reason, e.g - NSGroup does not exists.
LOG.warning(_LW("Failed to add %(target_type)s resources "
"(%(target_ids))s to NSGroup %(nsgroup_id)s"),
{'target_type': target_type,
'target_ids': target_ids,
'nsgroup_id': nsgroup_id})
raise exceptions.NSGroupIsFull(nsgroup_id=nsgroup_id)
def remove_member(self, nsgroup_id, target_type,
target_id, verify=False):
member_expr = self.get_member_expression(
target_type, target_id)
members = {'members': [member_expr]}
try:
return self._update_with_members(
nsgroup_id, members, consts.NSGROUP_REMOVE_MEMBERS)
except exceptions.ManagerError:
if verify:
raise exceptions.NSGroupMemberNotFound(member_id=target_id,
nsgroup_id=nsgroup_id)
def read(self, nsgroup_id):
return self.client.get(
'ns-groups/%s?populate_references=true' % nsgroup_id)
def delete(self, nsgroup_id):
try:
return self.client.delete(
'ns-groups/%s?force=true' % nsgroup_id)
# FIXME(roeyc): Should only except NotFound error.
except Exception:
LOG.debug("NSGroup %s does not exists for delete request.",
nsgroup_id)
class NsxLibFirewallSection(utils.NsxLibApiBase):
def _get_direction(self, sg_rule):
return (
consts.IN if sg_rule['direction'] == 'ingress'
else consts.OUT
)
def _get_l4_protocol_name(self, protocol_number):
if protocol_number is None:
return
protocol_number = constants.IP_PROTOCOL_MAP.get(protocol_number,
protocol_number)
protocol_number = int(protocol_number)
if protocol_number == 6:
return consts.TCP
elif protocol_number == 17:
return consts.UDP
elif protocol_number == 1:
return consts.ICMPV4
else:
return protocol_number
def get_nsservice(self, resource_type, **properties):
service = {'resource_type': resource_type}
service.update(properties)
return {'service': service}
def _decide_service(self, sg_rule):
l4_protocol = self._get_l4_protocol_name(sg_rule['protocol'])
direction = self._get_direction(sg_rule)
if l4_protocol in [consts.TCP, consts.UDP]:
# If port_range_min is not specified then we assume all ports are
# matched, relying on neutron to perform validation.
source_ports = []
if sg_rule['port_range_min'] is None:
destination_ports = []
elif sg_rule['port_range_min'] != sg_rule['port_range_max']:
# NSX API requires a non-empty range (e.g - '22-23')
destination_ports = ['%(port_range_min)s-%(port_range_max)s'
% sg_rule]
else:
destination_ports = ['%(port_range_min)s' % sg_rule]
if direction == consts.OUT:
source_ports, destination_ports = destination_ports, []
return self.get_nsservice(
consts.L4_PORT_SET_NSSERVICE,
l4_protocol=l4_protocol,
source_ports=source_ports,
destination_ports=destination_ports)
elif l4_protocol == consts.ICMPV4:
return self.get_nsservice(
consts.ICMP_TYPE_NSSERVICE,
protocol=l4_protocol,
icmp_type=sg_rule['port_range_min'],
icmp_code=sg_rule['port_range_max'])
elif l4_protocol is not None:
return self.get_nsservice(
consts.IP_PROTOCOL_NSSERVICE,
protocol_number=l4_protocol)
def _build(self, display_name, description, applied_tos, tags):
return {'display_name': display_name,
'description': description,
'stateful': True,
'section_type': consts.FW_SECTION_LAYER3,
'applied_tos': [self.get_nsgroup_reference(t_id)
for t_id in applied_tos],
'tags': tags}
def create_empty(self, display_name, description,
applied_tos, tags,
operation=consts.FW_INSERT_BOTTOM,
other_section=None):
resource = 'firewall/sections?operation=%s' % operation
body = self._build(display_name, description,
applied_tos, tags)
if other_section:
resource += '&id=%s' % other_section
return self.client.create(resource, body)
def update(self, section_id, display_name=None, description=None,
applied_tos=None, rules=None):
#Using internal method so we can access max_attempts in the decorator
@utils.retry_upon_exception(
exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def _do_update():
resource = 'firewall/sections/%s' % section_id
section = self.read(section_id)
if rules is not None:
resource += '?action=update_with_rules'
section.update({'rules': rules})
if display_name is not None:
section['display_name'] = display_name
if description is not None:
section['description'] = description
if applied_tos is not None:
section['applied_tos'] = [self.get_nsgroup_reference(nsg_id)
for nsg_id in applied_tos]
if rules is not None:
return self.client.create(resource, section)
elif any(p is not None for p in (display_name, description,
applied_tos)):
return self.client.update(resource, section)
return _do_update()
def read(self, section_id):
resource = 'firewall/sections/%s' % section_id
return self.client.get(resource)
def list(self):
resource = 'firewall/sections'
return self.client.get(resource).get('results', [])
def delete(self, section_id):
resource = 'firewall/sections/%s?cascade=true' % section_id
return self.client.delete(resource)
def get_nsgroup_reference(self, nsgroup_id):
return {'target_id': nsgroup_id,
'target_type': consts.NSGROUP}
def get_ip_cidr_reference(self, ip_cidr_block, ip_protocol):
target_type = (consts.TARGET_TYPE_IPV4ADDRESS
if ip_protocol == consts.IPV4
else consts.TARGET_TYPE_IPV6ADDRESS)
return {'target_id': ip_cidr_block,
'target_type': target_type}
def get_rule_dict(
self, display_name, source=None,
destination=None,
direction=consts.IN_OUT,
ip_protocol=consts.IPV4_IPV6,
service=None, action=consts.FW_ACTION_ALLOW,
logged=False):
return {'display_name': display_name,
'sources': [source] if source else [],
'destinations': [destination] if destination else [],
'direction': direction,
'ip_protocol': ip_protocol,
'services': [service] if service else [],
'action': action,
'logged': logged}
def add_rule(self, rule, section_id):
resource = 'firewall/sections/%s/rules' % section_id
params = '?operation=insert_bottom'
return self.client.create(resource + params, rule)
def add_rules(self, rules, section_id):
resource = 'firewall/sections/%s/rules' % section_id
params = '?action=create_multiple&operation=insert_bottom'
return self.client.create(resource + params, {'rules': rules})
def delete_rule(self, section_id, rule_id):
resource = 'firewall/sections/%s/rules/%s' % (section_id, rule_id)
return self.client.delete(resource)
def get_rules(self, section_id):
resource = 'firewall/sections/%s/rules' % section_id
return self.client.get(resource)
def _get_fw_rule_from_sg_rule(self, sg_rule, nsgroup_id, rmt_nsgroup_id,
logged, action):
# IPV4 or IPV6
ip_protocol = sg_rule['ethertype'].upper()
direction = self._get_direction(sg_rule)
if sg_rule.get(consts.LOCAL_IP_PREFIX):
local_ip_prefix = self.get_ip_cidr_reference(
sg_rule[consts.LOCAL_IP_PREFIX],
ip_protocol)
else:
local_ip_prefix = None
source = None
local_group = self.get_nsgroup_reference(nsgroup_id)
if sg_rule['remote_ip_prefix'] is not None:
source = self.get_ip_cidr_reference(
sg_rule['remote_ip_prefix'], ip_protocol)
destination = local_ip_prefix or local_group
else:
if rmt_nsgroup_id:
source = self.get_nsgroup_reference(rmt_nsgroup_id)
destination = local_ip_prefix or local_group
if direction == consts.OUT:
source, destination = destination, source
service = self._decide_service(sg_rule)
name = sg_rule['id']
return self.get_rule_dict(name, source,
destination, direction,
ip_protocol, service,
action, logged)
def create_rules(self, context, section_id, nsgroup_id,
logging_enabled, action, security_group_rules,
ruleid_2_remote_nsgroup_map):
# 1. translate rules
# 2. insert in section
# 3. return the rules
firewall_rules = []
for sg_rule in security_group_rules:
remote_nsgroup_id = ruleid_2_remote_nsgroup_map[sg_rule['id']]
fw_rule = self._get_fw_rule_from_sg_rule(
sg_rule, nsgroup_id, remote_nsgroup_id,
logging_enabled, action)
firewall_rules.append(fw_rule)
return self.add_rules(firewall_rules, section_id)
def set_rule_logging(self, section_id, logging):
rules = self._process_rules_logging_for_update(
section_id, logging)
self.update(section_id, rules=rules)
def _process_rules_logging_for_update(self, section_id, logging_enabled):
rules = self.get_rules(section_id).get('results', [])
update_rules = False
for rule in rules:
if rule['logged'] != logging_enabled:
rule['logged'] = logging_enabled
update_rules = True
return rules if update_rules else None
def init_default(self, name, description, nested_groups,
log_sg_blocked_traffic):
fw_sections = self.list()
for section in fw_sections:
if section['display_name'] == name:
break
else:
tags = self.build_v3_api_version_tag()
section = self.create_empty(
name, description, nested_groups, tags)
block_rule = self.get_rule_dict(
'Block All', action=consts.FW_ACTION_DROP,
logged=log_sg_blocked_traffic)
# TODO(roeyc): Add additional rules to allow IPV6 NDP.
dhcp_client = self.get_nsservice(
consts.L4_PORT_SET_NSSERVICE,
l4_protocol=consts.UDP,
source_ports=[67],
destination_ports=[68])
dhcp_client_rule_in = self.get_rule_dict(
'DHCP Reply', direction=consts.IN,
service=dhcp_client)
dhcp_server = (
self.get_nsservice(
consts.L4_PORT_SET_NSSERVICE,
l4_protocol=consts.UDP,
source_ports=[68],
destination_ports=[67]))
dhcp_client_rule_out = self.get_rule_dict(
'DHCP Request', direction=consts.OUT,
service=dhcp_server)
self.update(section['id'],
name, section['description'],
applied_tos=nested_groups,
rules=[dhcp_client_rule_out,
dhcp_client_rule_in,
block_rule])
return section['id']

213
vmware_nsxlib/v3/utils.py Normal file
View File

@ -0,0 +1,213 @@
# Copyright 2016 VMware, Inc.
# 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.
import retrying
from neutron_lib import exceptions
from oslo_log import log
from vmware_nsxlib._i18n import _
from vmware_nsxlib.v3 import exceptions as nsxlib_exceptions
LOG = log.getLogger(__name__)
MAX_RESOURCE_TYPE_LEN = 20
MAX_TAG_LEN = 40
DEFAULT_MAX_ATTEMPTS = 10
def _validate_resource_type_length(resource_type):
# Add in a validation to ensure that we catch this at build time
if len(resource_type) > MAX_RESOURCE_TYPE_LEN:
raise exceptions.InvalidInput(
error_message=(_('Resource type cannot exceed %(max_len)s '
'characters: %(resource_type)s') %
{'max_len': MAX_RESOURCE_TYPE_LEN,
'resource_type': resource_type}))
def add_v3_tag(tags, resource_type, tag):
_validate_resource_type_length(resource_type)
tags.append({'scope': resource_type, 'tag': tag[:MAX_TAG_LEN]})
return tags
def update_v3_tags(current_tags, tags_update):
current_scopes = set([tag['scope'] for tag in current_tags])
updated_scopes = set([tag['scope'] for tag in tags_update])
tags = [{'scope': tag['scope'], 'tag': tag['tag']}
for tag in (current_tags + tags_update)
if tag['scope'] in (current_scopes ^ updated_scopes)]
modified_scopes = current_scopes & updated_scopes
for tag in tags_update:
if tag['scope'] in modified_scopes:
# If the tag value is empty or None, then remove the tag completely
if tag['tag']:
tag['tag'] = tag['tag'][:MAX_TAG_LEN]
tags.append(tag)
return tags
def retry_upon_exception(exc, delay=500, max_delay=2000,
max_attempts=DEFAULT_MAX_ATTEMPTS):
return retrying.retry(retry_on_exception=lambda e: isinstance(e, exc),
wait_exponential_multiplier=delay,
wait_exponential_max=max_delay,
stop_max_attempt_number=max_attempts)
def list_match(list1, list2):
# Check if list1 and list2 have identical elements, but relaxed on
# dict elements where list1's dict element can be a subset of list2's
# corresponding element.
if (not isinstance(list1, list) or not isinstance(list2, list) or
len(list1) != len(list2)):
return False
list1 = sorted(list1)
list2 = sorted(list2)
for (v1, v2) in zip(list1, list2):
if isinstance(v1, dict):
if not dict_match(v1, v2):
return False
elif isinstance(v1, list):
if not list_match(v1, v2):
return False
elif v1 != v2:
return False
return True
def dict_match(dict1, dict2):
# Check if dict1 is a subset of dict2.
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
return False
for k1, v1 in dict1.items():
if k1 not in dict2:
return False
v2 = dict2[k1]
if isinstance(v1, dict):
if not dict_match(v1, v2):
return False
elif isinstance(v1, list):
if not list_match(v1, v2):
return False
elif v1 != v2:
return False
return True
def get_name_and_uuid(name, uuid, tag=None, maxlen=80):
short_uuid = '_' + uuid[:5] + '...' + uuid[-5:]
maxlen = maxlen - len(short_uuid)
if tag:
maxlen = maxlen - len(tag) - 1
return name[:maxlen] + '_' + tag + short_uuid
else:
return name[:maxlen] + short_uuid
class NsxLibApiBase(object):
"""Base class for nsxlib api """
def __init__(self, client, nsxlib_config):
self.client = client
self.nsxlib_config = nsxlib_config
super(NsxLibApiBase, self).__init__()
def _update_resource_with_retry(self, resource, payload):
# Using internal method so we can access max_attempts in the decorator
@retry_upon_exception(nsxlib_exceptions.StaleRevision,
max_attempts=self.nsxlib_config.max_attempts)
def do_update():
revised_payload = self.client.get(resource)
for key_name in payload.keys():
revised_payload[key_name] = payload[key_name]
return self.client.update(resource, revised_payload)
return do_update()
def _get_resource_by_name_or_id(self, name_or_id, resource):
all_results = self.client.get(resource)['results']
matched_results = []
for rs in all_results:
if rs.get('id') == name_or_id:
# Matched by id - must be unique
return name_or_id
if rs.get('display_name') == name_or_id:
# Matched by name - add to the list to verify it is unique
matched_results.append(rs)
if len(matched_results) == 0:
err_msg = (_("Could not find %(resource)s %(name)s") %
{'name': name_or_id, 'resource': resource})
# TODO(aaron): improve exception handling...
raise exceptions.ManagerError(details=err_msg)
elif len(matched_results) > 1:
err_msg = (_("Found multiple %(resource)s named %(name)s") %
{'name': name_or_id, 'resource': resource})
# TODO(aaron): improve exception handling...
raise exceptions.ManagerError(details=err_msg)
return matched_results[0].get('id')
def build_v3_api_version_tag(self,):
"""Some resources are created on the manager
that do not have a corresponding plugin resource.
"""
return [{'scope': self.nsxlib_config.plugin_scope,
'tag': self.nsxlib_config.plugin_tag},
{'scope': "os-api-version",
'tag': self.nsxlib_config.plugin_ver}]
def is_internal_resource(self, nsx_resource):
"""Indicates whether the passed nsx-resource is internal
owned by the plugin for internal use.
"""
for tag in nsx_resource.get('tags', []):
if tag['scope'] == self.nsxlib_config.plugin_scope:
return tag['tag'] == self.nsxlib_config.plugin_tag
return False
def build_v3_tags_payload(self, resource, resource_type, project_name):
"""Construct the tags payload that will be pushed to NSX-v3
Add <resource_type>:<resource-id>, os-project-id:<project-id>,
os-project-name:<project_name> os-api-version:<plugin-api-version>
"""
_validate_resource_type_length(resource_type)
# There may be cases when the plugin creates the port, for example DHCP
if not project_name:
project_name = self.nsxlib_config.plugin_tag
tenant_id = resource.get('tenant_id', '')
# If tenant_id is present in resource and set to None, explicitly set
# the tenant_id in tags as ''.
if tenant_id is None:
tenant_id = ''
return [{'scope': resource_type,
'tag': resource.get('id', '')[:MAX_TAG_LEN]},
{'scope': 'os-project-id',
'tag': tenant_id[:MAX_TAG_LEN]},
{'scope': 'os-project-name',
'tag': project_name[:MAX_TAG_LEN]},
{'scope': 'os-api-version',
'tag': self.nsxlib_config.plugin_ver}]

17
vmware_nsxlib/version.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright 2016 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
version_info = pbr.version.VersionInfo('vmware-nsxlib')