Skip to content
Snippets Groups Projects
Commit 2b9adea1 authored by Alfonso Tierno's avatar Alfonso Tierno
Browse files

Merge branch 'contrail' into v7.0


Change-Id: I25cc61d45ac002c43875c610d34ac79e1d84457f
Signed-off-by: default avatartierno <alfonso.tiernosepulveda@telefonica.com>
parents 3c60222b 4f4ce172
No related branches found
Tags v7.1.0rc5
No related merge requests found
......@@ -58,6 +58,7 @@ RUN /root/RO/RO/osm_ro/scripts/install-osm-im.sh --develop && \
python3 -m pip install -e /root/RO/RO-SDN-odl_openflow && \
python3 -m pip install -e /root/RO/RO-SDN-floodlight_openflow && \
python3 -m pip install -e /root/RO/RO-SDN-arista && \
python3 -m pip install -e /root/RO/RO-SDN-juniper_contrail && \
rm -rf /root/.cache && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
......
##
# 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.
##
all: clean package
clean:
rm -rf dist deb_dist osm_rosdn_juniper_contrail-*.tar.gz osm_rosdn_juniper_contrail.egg-info .eggs
package:
python3 setup.py --command-packages=stdeb.command sdist_dsc
cd deb_dist/osm-rosdn-juniper-contrail*/ && dpkg-buildpackage -rfakeroot -uc -us
# Copyright 2020 ETSI
#
# 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 json
import copy
import logging
from time import time
from requests.exceptions import ConnectionError
class HttpException(Exception):
pass
class NotFound(HttpException):
pass
class AuthError(HttpException):
pass
class DuplicateFound(HttpException):
pass
class ServiceUnavailableException(HttpException):
pass
class ContrailHttp(object):
def __init__(self, auth_info, logger):
self._logger = logger
# default don't verify client cert
self._ssl_verify = False
# auth info: must contain auth_url and auth_dict
self.auth_url = auth_info["auth_url"]
self.auth_dict = auth_info["auth_dict"]
self.max_retries = 3
# Default token timeout
self.token_timeout = 3500
self.token = None
# TODO - improve configuration timeouts
def get_cmd(self, url, headers):
self._logger.debug("")
resp = self._request("GET", url, headers)
return resp.json()
def post_headers_cmd(self, url, headers, post_fields_dict=None):
self._logger.debug("")
# obfuscate password before logging dict
if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'):
post_fields_dict_copy = copy.deepcopy(post_fields_dict)
post_fields_dict['auth']['identity']['password']['user']['password'] = '******'
json_data_log = post_fields_dict_copy
else:
json_data_log = post_fields_dict
self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
resp = self._request("POST_HEADERS", url, headers, data=post_fields_dict)
return resp.text
def post_cmd(self, url, headers, post_fields_dict=None):
self._logger.debug("")
# obfuscate password before logging dict
if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'):
post_fields_dict_copy = copy.deepcopy(post_fields_dict)
post_fields_dict['auth']['identity']['password']['user']['password'] = '******'
json_data_log = post_fields_dict_copy
else:
json_data_log = post_fields_dict
self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
resp = self._request("POST", url, headers, data=post_fields_dict)
return resp.text
def delete_cmd(self, url, headers):
self._logger.debug("")
resp = self._request("DELETE", url, headers)
return resp.text
def _get_token(self, headers):
if self.auth_url:
self._logger.debug('Current Token:'.format(self.token))
auth_url = self.auth_url + 'auth/tokens'
if self.token is None or self._token_expired():
if not self.auth_url:
self.token = ""
resp = self._request_noauth(url=auth_url, op="POST", headers=headers,
data=self.auth_dict)
self.token = resp.headers.get('x-subject-token')
self.last_token_time = time.time()
self._logger.debug('Obtained token: '.format(self.token))
return self.token
def _token_expired(self):
current_time = time.time()
if self.last_token_time and (current_time - self.last_token_time < self.token_timeout):
return False
else:
return True
def _request(self, op, url, http_headers, data=None, retry_auth_error=True):
headers = http_headers.copy()
# Get authorization (include authentication headers)
# todo - añadir token de nuevo
#token = self._get_token(headers)
token = None
if token:
headers['X-Auth-Token'] = token
try:
return self._request_noauth(op, url, headers, data)
except AuthError:
# If there is an auth error retry just once
if retry_auth_error:
return self._request(self, op, url, headers, data, retry_auth_error=False)
def _request_noauth(self, op, url, headers, data=None):
# Method to execute http requests with error control
# Authentication error, always make just one retry
# ConnectionError or ServiceUnavailable make configured retries with sleep between them
# Other errors to raise:
# - NotFound
# - Conflict
retry = 0
while retry < self.max_retries:
retry += 1
# Execute operation
try:
self._logger.info("Request METHOD: {} URL: {}".format(op, url))
if (op == "GET"):
resp = self._http_get(url, headers, query_params=data)
elif (op == "POST"):
resp = self._http_post(url, headers, json_data=data)
elif (op == "POST_HEADERS"):
resp = self._http_post_headers(url, headers, json_data=data)
elif (op == "DELETE"):
resp = self._http_delete(url, headers, json_data=data)
else:
raise HttpException("Unsupported operation: {}".format(op))
self._logger.info("Response HTTPCODE: {}".format(resp.status_code))
# Check http return code
if resp:
return resp
else:
status_code = resp.status_code
if status_code == 401:
# Auth Error - set token to None to reload it and raise AuthError
self.token = None
raise AuthError("Auth error executing operation")
elif status_code == 409:
raise DuplicateFound("Duplicate resource url: {}, response: {}".format(url, resp.text))
elif status_code == 404:
raise NotFound("Not found resource url: {}, response: {}".format(url, resp.text))
elif resp.status_code in [502, 503]:
if not self.max_retries or retry >= self.max_retries:
raise ServiceUnavailableException("Service unavailable error url: {}".format(url))
continue
else:
raise HttpException("Error status_code: {}, error_text: {}".format(resp.status_code, resp.text))
except ConnectionError as e:
self._logger.error("Connection error executing request: {}".format(repr(e)))
if not self.max_retries or retry >= self.max_retries:
raise ConnectionError
continue
except Exception as e:
self._logger.error("Error executing request: {}".format(repr(e)))
raise e
def _http_get(self, url, headers, query_params=None):
return requests.get(url, headers=headers, params=query_params)
def _http_post_headers(self, url, headers, json_data=None):
return requests.head(url, json=json_data, headers=headers, verify=False)
def _http_post(self, url, headers, json_data=None):
return requests.post(url, json=json_data, headers=headers, verify=False)
def _http_delete(self, url, headers, json_data=None):
return requests.delete(url, json=json_data, headers=headers)
# Copyright 2020 ETSI
#
# 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 logging
import json
from osm_ro.wim.sdnconn import SdnConnectorError
from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp
from osm_rosdn_juniper_contrail.rest_lib import NotFound
from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound
from osm_rosdn_juniper_contrail.rest_lib import HttpException
class UnderlayApi:
""" Class with CRUD operations for the underlay API """
def __init__(self, url, config=None, user=None, password=None, logger=None):
self.logger = logger or logging.getLogger("openmano.sdnconn.junipercontrail.sdnapi")
self.controller_url = url
if not url:
raise SdnConnectorError("'url' must be provided")
if not url.startswith("http"):
url = "http://" + url
if not url.endswith("/"):
url = url + "/"
self.url = url
self.auth_url = None
self.project = None
self.domain = None
self.asn = None
self.fabric = None
if config:
self.auth_url = config.get("auth_url")
self.project = config.get("project")
self.domain = config.get("domain")
self.asn = config.get("asn")
self.fabric = config.get("fabric")
# Init http headers for all requests
self.http_header = {'Content-Type': 'application/json'}
if user:
self.user = user
if password:
self.password = password
self.logger.debug("Config parameters for the underlay controller: auth_url: {}, project: {},"
" domain: {}, user: {}, password: {}".format(self.auth_url, self.project,
self.domain, self.user, self.password))
auth_dict = {}
auth_dict['auth'] = {}
auth_dict['auth']['scope'] = {}
auth_dict['auth']['scope']['project'] = {}
auth_dict['auth']['scope']['project']['domain'] = {}
auth_dict['auth']['scope']['project']['domain']["id"] = self.domain
auth_dict['auth']['scope']['project']['name'] = self.project
auth_dict['auth']['identity'] = {}
auth_dict['auth']['identity']['methods'] = ['password']
auth_dict['auth']['identity']['password'] = {}
auth_dict['auth']['identity']['password']['user'] = {}
auth_dict['auth']['identity']['password']['user']['name'] = self.user
auth_dict['auth']['identity']['password']['user']['password'] = self.password
auth_dict['auth']['identity']['password']['user']['domain'] = {}
auth_dict['auth']['identity']['password']['user']['domain']['id'] = self.domain
self.auth_dict = auth_dict
# Init http lib
auth_info = {"auth_url": self.auth_url, "auth_dict": auth_dict}
self.http = ContrailHttp(auth_info, self.logger)
def check_auth(self):
response = self.http.get_cmd(url=self.auth_url, headers=self.http_header)
return response
# Helper methods for CRUD operations
def get_all_by_type(self, controller_url, type):
endpoint = controller_url + type
response = self.http.get_cmd(url=endpoint, headers=self.http_header)
return response.get(type)
def get_by_uuid(self, type, uuid):
try:
endpoint = self.controller_url + type + "/{}".format(uuid)
response = self.http.get_cmd(url=endpoint, headers=self.http_header)
return response.get(type)
except NotFound:
return None
def delete_by_uuid(self, controller_url, type, uuid):
endpoint = controller_url + type + "/{}".format(uuid)
self.http.delete_cmd(url=endpoint, headers=self.http_header)
def get_uuid_from_fqname(self, type, fq_name):
"""
Obtain uuid from fqname
Returns: If resource not found returns None
In case of error raises an Exception
"""
payload = {
"type": type,
"fq_name": fq_name
}
try:
endpoint = self.controller_url + "fqname-to-id"
resp = self.http.post_cmd(url=endpoint,
headers=self.http_header,
post_fields_dict=payload)
return json.loads(resp).get("uuid")
except NotFound:
return None
def get_by_fq_name(self, type, fq_name):
# Obtain uuid by fqdn and then get data by uuid
uuid = self.get_uuid_from_fqname(type, fq_name)
if uuid:
return self.get_by_uuid(type, uuid)
else:
return None
def delete_ref(self, type, uuid, ref_type, ref_uuid, ref_fq_name):
payload = {
"type": type,
"uuid": uuid,
"ref-type": ref_type,
"ref-fq-name": ref_fq_name,
"operation": "DELETE"
}
endpoint = self.controller_url + "ref-update"
resp = self.http.post_cmd(url=endpoint,
headers=self.http_header,
post_fields_dict=payload)
return resp
# Aux methods to avoid code duplication of name conventions
def get_vpg_name(self, switch_id, switch_port):
return "{}_{}".format(switch_id, switch_port)
def get_vmi_name(self, switch_id, switch_port, vlan):
return "{}_{}-{}".format(switch_id, switch_port, vlan)
# Virtual network operations
def create_virtual_network(self, name, vni):
self.logger.debug("create vname, name: {}, vni: {}".format(name, vni))
routetarget = '{}:{}'.format(self.asn, vni)
vnet_dict = {
"virtual-network": {
"virtual_network_properties": {
"vxlan_network_identifier": vni,
},
"parent_type": "project",
"fq_name": [
self.domain,
self.project,
name
],
"route_target_list": {
"route_target": [
"target:" + routetarget
]
}
}
}
endpoint = self.controller_url + 'virtual-networks'
resp = self.http.post_cmd(url=endpoint,
headers=self.http_header,
post_fields_dict=vnet_dict)
if not resp:
raise SdnConnectorError('Error creating virtual network: empty response')
vnet_info = json.loads(resp)
self.logger.debug("created vnet, vnet_info: {}".format(vnet_info))
return vnet_info.get("virtual-network").get('uuid'), vnet_info.get("virtual-network")
def get_virtual_networks(self):
return self.get_all_by_type('virtual-networks')
def get_virtual_network(self, network_id):
return self.get_by_uuid('virtual-network', network_id)
def delete_virtual_network(self, network_id):
self.logger.debug("delete vnet uuid: {}".format(network_id))
self.delete_by_uuid(self.controller_url, 'virtual-network', network_id)
self.logger.debug("deleted vnet uuid: {}".format(network_id))
# Vpg operations
def create_vpg(self, switch_id, switch_port):
self.logger.debug("create vpg, switch_id: {}, switch_port: {}".format(switch_id, switch_port))
vpg_name = self.get_vpg_name(switch_id, switch_port)
vpg_dict = {
"virtual-port-group": {
"parent_type": "fabric",
"fq_name": [
"default-global-system-config",
self.fabric,
vpg_name
]
}
}
endpoint = self.controller_url + 'virtual-port-groups'
resp = self.http.post_cmd(url=endpoint,
headers=self.http_header,
post_fields_dict=vpg_dict)
if not resp:
raise SdnConnectorError('Error creating virtual port group: empty response')
vpg_info = json.loads(resp)
self.logger.debug("created vpg, vpg_info: {}".format(vpg_info))
return vpg_info.get("virtual-port-group").get('uuid'), vpg_info.get("virtual-port-group")
def get_vpgs(self):
return self.get_all_by_type(self.controller_url, 'virtual-port-groups')
def get_vpg(self, vpg_id):
return self.get_by_uuid(self.controller_url, "virtual-port-group", vpg_id)
def get_vpg_by_name(self, vpg_name):
fq_name = [
"default-global-system-config",
self.fabric,
vpg_name
]
return self.get_by_fq_name("virtual-port-group", fq_name)
def delete_vpg(self, vpg_id):
self.logger.debug("delete vpg, uuid: {}".format(vpg_id))
self.delete_by_uuid(self.controller_url, 'virtual-port-group', vpg_id)
self.logger.debug("deleted vpg, uuid: {}".format(vpg_id))
def create_vmi(self, switch_id, switch_port, network, vlan):
self.logger.debug("create vmi, switch_id: {}, switch_port: {}, network: {}, vlan: {}".format(
switch_id, switch_port, network, vlan))
vmi_name = self.get_vmi_name(switch_id, switch_port, vlan)
vpg_name = self.get_vpg_name(switch_id, switch_port)
profile_dict = {
"local_link_information": [
{
"port_id": switch_port,
"switch_id": switch_port,
"switch_info": switch_id,
"fabric": self.fabric
}
]
}
vmi_dict = {
"virtual-machine-interface": {
"parent_type": "project",
"fq_name": [
self.domain,
self.project,
vmi_name
],
"virtual_network_refs": [
{
"to": [
self.domain,
self.project,
network
]
}
],
"virtual_machine_interface_properties": {
"sub_interface_vlan_tag": vlan
},
"virtual_machine_interface_bindings": {
"key_value_pair": [
{
"key": "vnic_type",
"value": "baremetal"
},
{
"key": "vif_type",
"value": "vrouter"
},
{
"key": "vpg",
"value": vpg_name
},
{
"key": "profile",
"value": json.dumps(profile_dict)
}
]
}
}
}
endpoint = self.controller_url + 'virtual-machine-interfaces'
self.logger.debug("vmi_dict: {}".format(vmi_dict))
resp = self.http.post_cmd(url=endpoint,
headers=self.http_header,
post_fields_dict=vmi_dict)
if not resp:
raise SdnConnectorError('Error creating vmi: empty response')
vmi_info = json.loads(resp)
self.logger.debug("created vmi, info: {}".format(vmi_info))
return vmi_info.get("virtual-machine-interface").get('uuid'), vmi_info.get("virtual-machine-interface")
def get_vmi(self, vmi_uuid):
return self.get_by_uuid(self.controller_url, 'virtual-machine-interface', vmi_uuid)
def delete_vmi(self, uuid):
self.logger.debug("delete vmi uuid: {}".format(uuid))
self.delete_by_uuid(self.controller_url, 'virtual-machine-interface', uuid)
self.logger.debug("deleted vmi: {}".format(uuid))
def unref_vmi_vpg(self, vpg_id, vmi_id, vmi_fq_name):
self.delete_ref("virtual-port-group", vpg_id, "virtual-machine-interface", vmi_id, vmi_fq_name)
##
# 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.
##
requests
git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO
#!/usr/bin/env python3
# -*- 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.
##
from setuptools import setup
_name = "osm_rosdn_juniper_contrail"
README = """
===========
osm-rosdn_juniper_contrail
===========
osm-ro plugin for Juniper Contrail SDN
"""
setup(
name=_name,
description='OSM RO SDN plugin for Juniper Contrail',
long_description=README,
version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'),
# version=VERSION,
# python_requires='>3.5.0',
author='ETSI OSM',
author_email='OSM_TECH@list.etsi.org',
maintainer='ETSI OSM',
maintainer_email='OSM_TECH@list.etsi.org',
url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary',
license='Apache 2.0',
packages=[_name],
include_package_data=True,
#dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"],
install_requires=[
"requests",
"osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO"
],
setup_requires=['setuptools-version-command'],
entry_points={
'osm_rosdn.plugins': ['rosdn_juniper_contrail = osm_rosdn_juniper_contrail.sdn_assist_juniper_contrail:JuniperContrail'],
},
)
#
# 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.
#
[DEFAULT]
X-Python3-Version : >= 3.5
Depends3: python3-requests, python3-osm-ro
##
# 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.
##
[tox]
envlist = py3
toxworkdir={homedir}/.tox
[testenv]
basepython = python3
install_command = python3 -m pip install -r requirements.txt -U {opts} {packages}
# deps = -r{toxinidir}/test-requirements.txt
commands=python3 -m unittest discover -v
[testenv:flake8]
basepython = python3
deps = flake8
commands = flake8 osm_rosdn_juniper_contrail --max-line-length 120 \
--exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504
[testenv:unittest]
basepython = python3
commands = python3 -m unittest osm_rosdn_juniper_contrail.tests
[testenv:build]
basepython = python3
deps = stdeb
setuptools-version-command
commands = python3 setup.py --command-packages=stdeb.command bdist_deb
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment