# Copyright 2015 Infoblox 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 functools
import re
import requests
from requests import exceptions as req_exc
import six
import urllib
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from oslo_log import log as logging
from oslo_serialization import jsonutils
from infoblox_client import exceptions as ib_ex
from infoblox_client import utils
LOG = logging.getLogger(__name__)
CLOUD_WAPI_MAJOR_VERSION = 2
[docs]def reraise_neutron_exception(func):
@functools.wraps(func)
def callee(*args, **kwargs):
try:
return func(*args, **kwargs)
except req_exc.Timeout as e:
raise ib_ex.InfobloxTimeoutError(e)
except req_exc.RequestException as e:
raise ib_ex.InfobloxConnectionError(reason=e)
return callee
[docs]class Connector(object):
"""Connector stands for interacting with Infoblox NIOS
Defines methods for getting, creating, updating and
removing objects from an Infoblox server instance.
"""
DEFAULT_HEADER = {'Content-type': 'application/json'}
DEFAULT_OPTIONS = {'ssl_verify': False,
'silent_ssl_warnings': False,
'http_request_timeout': 10,
'http_pool_connections': 10,
'http_pool_maxsize': 10,
'max_retries': 3,
'wapi_version': '1.4',
'max_results': None,
'log_api_calls_as_info': False}
def __init__(self, options):
self._parse_options(options)
self._configure_session()
# urllib has different interface for py27 and py34
try:
self._urlencode = urllib.urlencode
self._quote = urllib.quote
self._urljoin = urlparse.urljoin
except AttributeError:
self._urlencode = urlparse.urlencode
self._quote = urlparse.quote
self._urljoin = urlparse.urljoin
def _parse_options(self, options):
"""Copy needed options to self"""
attributes = ('host', 'wapi_version', 'username', 'password',
'ssl_verify', 'http_request_timeout', 'max_retries',
'http_pool_connections', 'http_pool_maxsize',
'silent_ssl_warnings', 'log_api_calls_as_info',
'max_results')
for attr in attributes:
if isinstance(options, dict) and attr in options:
setattr(self, attr, options[attr])
elif hasattr(options, attr):
value = getattr(options, attr)
setattr(self, attr, value)
elif attr in self.DEFAULT_OPTIONS:
setattr(self, attr, self.DEFAULT_OPTIONS[attr])
else:
msg = "WAPI config error. Option %s is not defined" % attr
raise ib_ex.InfobloxConfigException(msg=msg)
for attr in ('host', 'username', 'password'):
if not getattr(self, attr):
msg = "WAPI config error. Option %s can not be blank" % attr
raise ib_ex.InfobloxConfigException(msg=msg)
self.wapi_url = "https://%s/wapi/v%s/" % (self.host,
self.wapi_version)
self.cloud_api_enabled = self.is_cloud_wapi(self.wapi_version)
def _configure_session(self):
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=self.http_pool_connections,
pool_maxsize=self.http_pool_maxsize,
max_retries=self.max_retries)
self.session.mount('http://', adapter)
self.session.mount('https://', adapter)
self.session.auth = (self.username, self.password)
self.session.verify = utils.try_value_to_bool(self.ssl_verify,
strict_mode=False)
if self.silent_ssl_warnings:
requests.packages.urllib3.disable_warnings()
def _construct_url(self, relative_path, query_params=None,
extattrs=None, force_proxy=False):
if query_params is None:
query_params = {}
if extattrs is None:
extattrs = {}
if force_proxy:
query_params['_proxy_search'] = 'GM'
if not relative_path or relative_path[0] == '/':
raise ValueError('Path in request must be relative.')
query = ''
if query_params or extattrs:
query = '?'
if extattrs:
attrs_queries = []
for key, value in extattrs.items():
attrs_queries.append('*' + key + '=' + value['value'])
query += '&'.join(attrs_queries)
if query_params:
if len(query) > 1:
query += '&'
query += self._urlencode(query_params)
base_url = self._urljoin(self.wapi_url,
self._quote(relative_path))
return base_url + query
@staticmethod
def _validate_obj_type_or_die(obj_type, obj_type_expected=True):
if not obj_type:
raise ValueError('NIOS object type cannot be empty.')
if obj_type_expected and '/' in obj_type:
raise ValueError('NIOS object type cannot contain slash.')
@staticmethod
def _validate_authorized(response):
if response.status_code == requests.codes.UNAUTHORIZED:
raise ib_ex.InfobloxBadWAPICredential(response='')
@staticmethod
def _build_query_params(payload=None, return_fields=None,
max_results=None):
if payload:
query_params = payload
else:
query_params = dict()
if return_fields:
query_params['_return_fields'] = ','.join(return_fields)
if max_results:
query_params['_max_results'] = max_results
return query_params
def _get_request_options(self, data=None):
opts = dict(timeout=self.http_request_timeout,
headers=self.DEFAULT_HEADER)
if data:
opts['data'] = jsonutils.dumps(data)
return opts
@staticmethod
def _parse_reply(request):
"""Tries to parse reply from NIOS.
Raises exception with content if reply is not in json format
"""
try:
return jsonutils.loads(request.content)
except ValueError:
raise ib_ex.InfobloxConnectionError(reason=request.content)
def _log_request(self, type, url, opts):
message = ("Sending %s request to %s with parameters %s",
type, url, opts)
if self.log_api_calls_as_info:
LOG.info(*message)
else:
LOG.debug(*message)
@reraise_neutron_exception
[docs] def get_object(self, obj_type, payload=None, return_fields=None,
extattrs=None, force_proxy=False, max_results=None):
"""Retrieve a list of Infoblox objects of type 'obj_type'
Some get requests like 'ipv4address' should be always
proxied to GM on Hellfire
If request is cloud and proxy is not forced yet,
then plan to do 2 request:
- the first one is not proxied to GM
- the second is proxied to GM
Args:
obj_type (str): Infoblox object type, e.g. 'network',
'range', etc.
payload (dict): Payload with data to send
return_fields (list): List of fields to be returned
extattrs (list): List of Extensible Attributes
force_proxy (bool): Set _proxy_search flag
to process requests on GM
max_results (int): Maximum number of objects to be returned.
If set to a negative number the appliance will return an error
when the number of returned objects would exceed the setting.
The default is -1000. If this is set to a positive number,
the results will be truncated when necessary.
Returns:
A list of the Infoblox objects requested
Raises:
InfobloxObjectNotFound
"""
self._validate_obj_type_or_die(obj_type, obj_type_expected=False)
# max_results passed to get_object has priority over
# one defined as connector option
if max_results is None and self.max_results:
max_results = self.max_results
query_params = self._build_query_params(payload=payload,
return_fields=return_fields,
max_results=max_results)
# Clear proxy flag if wapi version is too old (non-cloud)
proxy_flag = self.cloud_api_enabled and force_proxy
url = self._construct_url(obj_type, query_params,
extattrs, proxy_flag)
ib_object = self._get_object(obj_type, url)
if ib_object:
return ib_object
# Do second get call with force_proxy if not done yet
if self.cloud_api_enabled and not force_proxy:
url = self._construct_url(obj_type, query_params, extattrs,
force_proxy=True)
ib_object = self._get_object(obj_type, url)
if ib_object:
return ib_object
return None
def _get_object(self, obj_type, url):
opts = self._get_request_options()
self._log_request('get', url, opts)
r = self.session.get(url, **opts)
self._validate_authorized(r)
if r.status_code != requests.codes.ok:
LOG.error("Error occurred on object search: %s", r.content)
return None
return self._parse_reply(r)
@reraise_neutron_exception
[docs] def create_object(self, obj_type, payload, return_fields=None):
"""Create an Infoblox object of type 'obj_type'
Args:
obj_type (str): Infoblox object type,
e.g. 'network', 'range', etc.
payload (dict): Payload with data to send
return_fields (list): List of fields to be returned
Returns:
The object reference of the newly create object
Raises:
InfobloxException
"""
self._validate_obj_type_or_die(obj_type)
query_params = self._build_query_params(return_fields=return_fields)
url = self._construct_url(obj_type, query_params)
opts = self._get_request_options(data=payload)
self._log_request('post', url, opts)
r = self.session.post(url, **opts)
self._validate_authorized(r)
if r.status_code != requests.codes.CREATED:
response = utils.safe_json_load(r.content)
already_assigned = 'is assigned to another network view'
if response and already_assigned in response.get('text'):
exception = ib_ex.InfobloxMemberAlreadyAssigned
else:
exception = ib_ex.InfobloxCannotCreateObject
raise exception(
response=response,
obj_type=obj_type,
content=r.content,
args=payload,
code=r.status_code)
return self._parse_reply(r)
@reraise_neutron_exception
[docs] def call_func(self, func_name, ref, payload, return_fields=None):
query_params = self._build_query_params(return_fields=return_fields)
query_params['_function'] = func_name
url = self._construct_url(ref, query_params)
opts = self._get_request_options(data=payload)
self._log_request('post', url, opts)
r = self.session.post(url, **opts)
self._validate_authorized(r)
if r.status_code not in (requests.codes.CREATED,
requests.codes.ok):
raise ib_ex.InfobloxFuncException(
response=jsonutils.loads(r.content),
ref=ref,
func_name=func_name,
content=r.content,
code=r.status_code)
return self._parse_reply(r)
@reraise_neutron_exception
[docs] def update_object(self, ref, payload, return_fields=None):
"""Update an Infoblox object
Args:
ref (str): Infoblox object reference
payload (dict): Payload with data to send
Returns:
The object reference of the updated object
Raises:
InfobloxException
"""
query_params = self._build_query_params(return_fields=return_fields)
opts = self._get_request_options(data=payload)
url = self._construct_url(ref, query_params)
self._log_request('put', url, opts)
r = self.session.put(url, **opts)
self._validate_authorized(r)
if r.status_code != requests.codes.ok:
raise ib_ex.InfobloxCannotUpdateObject(
response=jsonutils.loads(r.content),
ref=ref,
content=r.content,
code=r.status_code)
return self._parse_reply(r)
@reraise_neutron_exception
[docs] def delete_object(self, ref):
"""Remove an Infoblox object
Args:
ref (str): Object reference
Returns:
The object reference of the removed object
Raises:
InfobloxException
"""
opts = self._get_request_options()
url = self._construct_url(ref)
self._log_request('delete', url, opts)
r = self.session.delete(url, **opts)
self._validate_authorized(r)
if r.status_code != requests.codes.ok:
raise ib_ex.InfobloxCannotDeleteObject(
response=jsonutils.loads(r.content),
ref=ref,
content=r.content,
code=r.status_code)
return self._parse_reply(r)
@staticmethod
[docs] def is_cloud_wapi(wapi_version):
valid = wapi_version and isinstance(wapi_version, six.string_types)
if not valid:
raise ValueError("Invalid argument was passed")
version_match = re.search('(\d+)\.(\d+)', wapi_version)
if version_match:
if int(version_match.group(1)) >= \
CLOUD_WAPI_MAJOR_VERSION:
return True
return False