1163 lines
42 KiB
Python
1163 lines
42 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
|
# (the "License"); you may not use this file except in compliance withv
|
|
# 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 libcloud.utils.py3 import ET
|
|
from libcloud.common.dimensiondata import DimensionDataConnection
|
|
from libcloud.common.dimensiondata import DimensionDataPool
|
|
from libcloud.common.dimensiondata import DimensionDataPoolMember
|
|
from libcloud.common.dimensiondata import DimensionDataVirtualListener
|
|
from libcloud.common.dimensiondata import DimensionDataVIPNode
|
|
from libcloud.common.dimensiondata import DimensionDataDefaultHealthMonitor
|
|
from libcloud.common.dimensiondata import DimensionDataPersistenceProfile
|
|
from libcloud.common.dimensiondata import \
|
|
DimensionDataVirtualListenerCompatibility
|
|
from libcloud.common.dimensiondata import DimensionDataDefaultiRule
|
|
from libcloud.common.dimensiondata import API_ENDPOINTS
|
|
from libcloud.common.dimensiondata import DEFAULT_REGION
|
|
from libcloud.common.dimensiondata import TYPES_URN
|
|
from libcloud.utils.misc import reverse_dict
|
|
from libcloud.utils.xml import fixxpath, findtext, findall
|
|
from libcloud.loadbalancer.types import State
|
|
from libcloud.loadbalancer.base import Algorithm, Driver, LoadBalancer
|
|
from libcloud.loadbalancer.base import Member
|
|
from libcloud.loadbalancer.types import Provider
|
|
|
|
|
|
class DimensionDataLBDriver(Driver):
|
|
"""
|
|
DimensionData node driver.
|
|
"""
|
|
|
|
selected_region = None
|
|
connectionCls = DimensionDataConnection
|
|
name = 'Dimension Data Load Balancer'
|
|
website = 'https://cloud.dimensiondata.com/'
|
|
type = Provider.DIMENSIONDATA
|
|
api_version = 1.0
|
|
|
|
network_domain_id = None
|
|
|
|
_VALUE_TO_ALGORITHM_MAP = {
|
|
'ROUND_ROBIN': Algorithm.ROUND_ROBIN,
|
|
'LEAST_CONNECTIONS': Algorithm.LEAST_CONNECTIONS,
|
|
'SHORTEST_RESPONSE': Algorithm.SHORTEST_RESPONSE,
|
|
'PERSISTENT_IP': Algorithm.PERSISTENT_IP
|
|
}
|
|
_ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP)
|
|
|
|
_VALUE_TO_STATE_MAP = {
|
|
'NORMAL': State.RUNNING,
|
|
'PENDING_ADD': State.PENDING,
|
|
'PENDING_CHANGE': State.PENDING,
|
|
'PENDING_DELETE': State.PENDING,
|
|
'FAILED_ADD': State.ERROR,
|
|
'FAILED_CHANGE': State.ERROR,
|
|
'FAILED_DELETE': State.ERROR,
|
|
'REQUIRES_SUPPORT': State.ERROR
|
|
}
|
|
|
|
def __init__(self, key, secret=None, secure=True, host=None, port=None,
|
|
api_version=None, region=DEFAULT_REGION, **kwargs):
|
|
|
|
if region not in API_ENDPOINTS and host is None:
|
|
raise ValueError(
|
|
'Invalid region: %s, no host specified' % (region))
|
|
if region is not None:
|
|
self.selected_region = API_ENDPOINTS[region]
|
|
|
|
super(DimensionDataLBDriver, self).__init__(key=key, secret=secret,
|
|
secure=secure, host=host,
|
|
port=port,
|
|
api_version=api_version,
|
|
region=region,
|
|
**kwargs)
|
|
|
|
def _ex_connection_class_kwargs(self):
|
|
"""
|
|
Add the region to the kwargs before the connection is instantiated
|
|
"""
|
|
|
|
kwargs = super(DimensionDataLBDriver,
|
|
self)._ex_connection_class_kwargs()
|
|
kwargs['region'] = self.selected_region
|
|
return kwargs
|
|
|
|
def create_balancer(self, name, port=None, protocol=None,
|
|
algorithm=None, members=None,
|
|
ex_listener_ip_address=None):
|
|
"""
|
|
Create a new load balancer instance
|
|
|
|
:param name: Name of the new load balancer (required)
|
|
:type name: ``str``
|
|
|
|
:param port: An integer in the range of 1-65535. If not supplied,
|
|
it will be taken to mean 'Any Port'
|
|
:type port: ``int``
|
|
|
|
:param protocol: Loadbalancer protocol, defaults to http.
|
|
:type protocol: ``str``
|
|
|
|
:param members: list of Members to attach to balancer (optional)
|
|
:type members: ``list`` of :class:`Member`
|
|
|
|
:param algorithm: Load balancing algorithm, defaults to ROUND_ROBIN.
|
|
:type algorithm: :class:`.Algorithm`
|
|
|
|
:param ex_listener_ip_address: Must be a valid IPv4 in dot-decimal
|
|
notation (x.x.x.x).
|
|
:type ex_listener_ip_address: ``str``
|
|
|
|
:rtype: :class:`LoadBalancer`
|
|
"""
|
|
network_domain_id = self.network_domain_id
|
|
if protocol is None:
|
|
protocol = 'http'
|
|
if algorithm is None:
|
|
algorithm = Algorithm.ROUND_ROBIN
|
|
|
|
# Create a pool first
|
|
pool = self.ex_create_pool(
|
|
network_domain_id=network_domain_id,
|
|
name=name,
|
|
ex_description=None,
|
|
balancer_method=self._ALGORITHM_TO_VALUE_MAP[algorithm])
|
|
|
|
# Attach the members to the pool as nodes
|
|
if members is not None:
|
|
for member in members:
|
|
node = self.ex_create_node(
|
|
network_domain_id=network_domain_id,
|
|
name=member.ip,
|
|
ip=member.ip,
|
|
ex_description=None)
|
|
self.ex_create_pool_member(
|
|
pool=pool,
|
|
node=node,
|
|
port=port)
|
|
|
|
# Create the virtual listener (balancer)
|
|
listener = self.ex_create_virtual_listener(
|
|
network_domain_id=network_domain_id,
|
|
name=name,
|
|
ex_description=name,
|
|
port=port,
|
|
pool=pool,
|
|
listener_ip_address=ex_listener_ip_address)
|
|
|
|
return LoadBalancer(
|
|
id=listener.id,
|
|
name=listener.name,
|
|
state=State.RUNNING,
|
|
ip=listener.ip,
|
|
port=port,
|
|
driver=self,
|
|
extra={'pool_id': pool.id,
|
|
'network_domain_id': network_domain_id,
|
|
'listener_ip_address': ex_listener_ip_address}
|
|
)
|
|
|
|
def list_balancers(self, ex_network_domain_id=None):
|
|
"""
|
|
List all loadbalancers inside a geography or in given network.
|
|
|
|
In Dimension Data terminology these are known as virtual listeners
|
|
|
|
:param ex_network_domain_id: UUID of Network Domain
|
|
if not None returns only balancers in the given network
|
|
if None then returns all pools for the organization
|
|
:type ex_network_domain_id: ``str``
|
|
|
|
:rtype: ``list`` of :class:`LoadBalancer`
|
|
"""
|
|
params = None
|
|
if ex_network_domain_id is not None:
|
|
params = {"networkDomainId": ex_network_domain_id}
|
|
|
|
return self._to_balancers(
|
|
self.connection
|
|
.request_with_orgId_api_2('networkDomainVip/virtualListener',
|
|
params=params).object)
|
|
|
|
def get_balancer(self, balancer_id):
|
|
"""
|
|
Return a :class:`LoadBalancer` object.
|
|
|
|
:param balancer_id: id of a load balancer you want to fetch
|
|
:type balancer_id: ``str``
|
|
|
|
:rtype: :class:`LoadBalancer`
|
|
"""
|
|
|
|
bal = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/virtualListener/%s'
|
|
% balancer_id).object
|
|
return self._to_balancer(bal)
|
|
|
|
def list_protocols(self):
|
|
"""
|
|
Return a list of supported protocols.
|
|
|
|
Since all protocols are support by Dimension Data, this is a list
|
|
of common protocols.
|
|
|
|
:rtype: ``list`` of ``str``
|
|
"""
|
|
return ['http', 'https', 'tcp', 'udp', 'ftp', 'smtp']
|
|
|
|
def balancer_list_members(self, balancer):
|
|
"""
|
|
Return list of members attached to balancer.
|
|
|
|
In Dimension Data terminology these are the members of the pools
|
|
within a virtual listener.
|
|
|
|
:param balancer: LoadBalancer which should be used
|
|
:type balancer: :class:`LoadBalancer`
|
|
|
|
:rtype: ``list`` of :class:`Member`
|
|
"""
|
|
pool_members = self.ex_get_pool_members(balancer.extra['pool_id'])
|
|
members = []
|
|
for pool_member in pool_members:
|
|
members.append(Member(
|
|
id=pool_member.id,
|
|
ip=pool_member.ip,
|
|
port=pool_member.port,
|
|
balancer=balancer,
|
|
extra=None
|
|
))
|
|
return members
|
|
|
|
def balancer_attach_member(self, balancer, member):
|
|
"""
|
|
Attach a member to balancer
|
|
|
|
:param balancer: LoadBalancer which should be used
|
|
:type balancer: :class:`LoadBalancer`
|
|
|
|
:param member: Member to join to the balancer
|
|
:type member: :class:`Member`
|
|
|
|
:return: Member after joining the balancer.
|
|
:rtype: :class:`Member`
|
|
"""
|
|
node = self.ex_create_node(
|
|
network_domain_id=balancer.extra['network_domain_id'],
|
|
name='Member.' + member.ip,
|
|
ip=member.ip,
|
|
ex_description=''
|
|
)
|
|
if node is False:
|
|
return False
|
|
pool = self.ex_get_pool(balancer.extra['pool_id'])
|
|
pool_member = self.ex_create_pool_member(
|
|
pool=pool,
|
|
node=node,
|
|
port=member.port)
|
|
member.id = pool_member.id
|
|
return member
|
|
|
|
def balancer_detach_member(self, balancer, member):
|
|
"""
|
|
Detach member from balancer
|
|
|
|
:param balancer: LoadBalancer which should be used
|
|
:type balancer: :class:`LoadBalancer`
|
|
|
|
:param member: Member which should be used
|
|
:type member: :class:`Member`
|
|
|
|
:return: ``True`` if member detach was successful, otherwise ``False``.
|
|
:rtype: ``bool``
|
|
"""
|
|
create_pool_m = ET.Element('removePoolMember', {'xmlns': TYPES_URN,
|
|
'id': member.id})
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
'networkDomainVip/removePoolMember',
|
|
method='POST',
|
|
data=ET.tostring(create_pool_m)).object
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def destroy_balancer(self, balancer):
|
|
"""
|
|
Destroy a load balancer (virtual listener)
|
|
|
|
:param balancer: LoadBalancer which should be used
|
|
:type balancer: :class:`LoadBalancer`
|
|
|
|
:return: ``True`` if the destroy was successful, otherwise ``False``.
|
|
:rtype: ``bool``
|
|
"""
|
|
delete_listener = ET.Element('deleteVirtualListener',
|
|
{'xmlns': TYPES_URN,
|
|
'id': balancer.id})
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
'networkDomainVip/deleteVirtualListener',
|
|
method='POST',
|
|
data=ET.tostring(delete_listener)).object
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_set_current_network_domain(self, network_domain_id):
|
|
"""
|
|
Set the network domain (part of the network) of the driver
|
|
|
|
:param network_domain_id: ID of the pool (required)
|
|
:type network_domain_id: ``str``
|
|
"""
|
|
self.network_domain_id = network_domain_id
|
|
|
|
def ex_get_current_network_domain(self):
|
|
"""
|
|
Get the current network domain ID of the driver.
|
|
|
|
:return: ID of the network domain
|
|
:rtype: ``str``
|
|
"""
|
|
return self.network_domain_id
|
|
|
|
def ex_create_pool_member(self, pool, node, port=None):
|
|
"""
|
|
Create a new member in an existing pool from an existing node
|
|
|
|
:param pool: Instance of ``DimensionDataPool`` (required)
|
|
:type pool: ``DimensionDataPool``
|
|
|
|
:param node: Instance of ``DimensionDataVIPNode`` (required)
|
|
:type node: ``DimensionDataVIPNode``
|
|
|
|
:param port: Port the the service will listen on
|
|
:type port: ``str``
|
|
|
|
:return: The node member, instance of ``DimensionDataPoolMember``
|
|
:rtype: ``DimensionDataPoolMember``
|
|
"""
|
|
create_pool_m = ET.Element('addPoolMember', {'xmlns': TYPES_URN})
|
|
ET.SubElement(create_pool_m, "poolId").text = pool.id
|
|
ET.SubElement(create_pool_m, "nodeId").text = node.id
|
|
if port is not None:
|
|
ET.SubElement(create_pool_m, "port").text = str(port)
|
|
ET.SubElement(create_pool_m, "status").text = 'ENABLED'
|
|
|
|
response = self.connection.request_with_orgId_api_2(
|
|
'networkDomainVip/addPoolMember',
|
|
method='POST',
|
|
data=ET.tostring(create_pool_m)).object
|
|
|
|
member_id = None
|
|
node_name = None
|
|
for info in findall(response, 'info', TYPES_URN):
|
|
if info.get('name') == 'poolMemberId':
|
|
member_id = info.get('value')
|
|
if info.get('name') == 'nodeName':
|
|
node_name = info.get('value')
|
|
|
|
return DimensionDataPoolMember(
|
|
id=member_id,
|
|
name=node_name,
|
|
status=State.RUNNING,
|
|
ip=node.ip,
|
|
port=port,
|
|
node_id=node.id
|
|
)
|
|
|
|
def ex_create_node(self,
|
|
network_domain_id,
|
|
name,
|
|
ip,
|
|
ex_description,
|
|
connection_limit=25000,
|
|
connection_rate_limit=2000):
|
|
"""
|
|
Create a new node
|
|
|
|
:param network_domain_id: Network Domain ID (required)
|
|
:type name: ``str``
|
|
|
|
:param name: name of the node (required)
|
|
:type name: ``str``
|
|
|
|
:param ip: IPv4 address of the node (required)
|
|
:type ip: ``str``
|
|
|
|
:param ex_description: Description of the node (required)
|
|
:type ex_description: ``str``
|
|
|
|
:param connection_limit: Maximum number
|
|
of concurrent connections per sec
|
|
:type connection_limit: ``int``
|
|
|
|
:param connection_rate_limit: Maximum number of concurrent sessions
|
|
:type connection_rate_limit: ``int``
|
|
|
|
:return: Instance of ``DimensionDataVIPNode``
|
|
:rtype: ``DimensionDataVIPNode``
|
|
"""
|
|
create_node_elm = ET.Element('createNode', {'xmlns': TYPES_URN})
|
|
ET.SubElement(create_node_elm, "networkDomainId") \
|
|
.text = network_domain_id
|
|
ET.SubElement(create_node_elm, "name").text = name
|
|
ET.SubElement(create_node_elm, "description").text \
|
|
= str(ex_description)
|
|
ET.SubElement(create_node_elm, "ipv4Address").text = ip
|
|
ET.SubElement(create_node_elm, "status").text = 'ENABLED'
|
|
ET.SubElement(create_node_elm, "connectionLimit") \
|
|
.text = str(connection_limit)
|
|
ET.SubElement(create_node_elm, "connectionRateLimit") \
|
|
.text = str(connection_rate_limit)
|
|
|
|
response = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/createNode',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
|
|
node_id = None
|
|
node_name = None
|
|
for info in findall(response, 'info', TYPES_URN):
|
|
if info.get('name') == 'nodeId':
|
|
node_id = info.get('value')
|
|
if info.get('name') == 'name':
|
|
node_name = info.get('value')
|
|
return DimensionDataVIPNode(
|
|
id=node_id,
|
|
name=node_name,
|
|
status=State.RUNNING,
|
|
ip=ip
|
|
)
|
|
|
|
def ex_update_node(self, node):
|
|
"""
|
|
Update the properties of a node
|
|
|
|
:param pool: The instance of ``DimensionDataNode`` to update
|
|
:type pool: ``DimensionDataNode``
|
|
|
|
:return: The instance of ``DimensionDataNode``
|
|
:rtype: ``DimensionDataNode``
|
|
"""
|
|
create_node_elm = ET.Element('editNode', {'xmlns': TYPES_URN})
|
|
ET.SubElement(create_node_elm, "connectionLimit") \
|
|
.text = str(node.connection_limit)
|
|
ET.SubElement(create_node_elm, "connectionRateLimit") \
|
|
.text = str(node.connection_rate_limit)
|
|
|
|
self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/createNode',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
return node
|
|
|
|
def ex_set_node_state(self, node, enabled):
|
|
"""
|
|
Change the state of a node (enable/disable)
|
|
|
|
:param pool: The instance of ``DimensionDataNode`` to update
|
|
:type pool: ``DimensionDataNode``
|
|
|
|
:param enabled: The target state of the node
|
|
:type enabled: ``bool``
|
|
|
|
:return: The instance of ``DimensionDataNode``
|
|
:rtype: ``DimensionDataNode``
|
|
"""
|
|
create_node_elm = ET.Element('editNode', {'xmlns': TYPES_URN})
|
|
ET.SubElement(create_node_elm, "status") \
|
|
.text = "ENABLED" if enabled is True else "DISABLED"
|
|
|
|
self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/editNode',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
return node
|
|
|
|
def ex_create_pool(self,
|
|
network_domain_id,
|
|
name,
|
|
balancer_method,
|
|
ex_description,
|
|
health_monitors=None,
|
|
service_down_action='NONE',
|
|
slow_ramp_time=30):
|
|
"""
|
|
Create a new pool
|
|
|
|
:param network_domain_id: Network Domain ID (required)
|
|
:type name: ``str``
|
|
|
|
:param name: name of the node (required)
|
|
:type name: ``str``
|
|
|
|
:param balancer_method: The load balancer algorithm (required)
|
|
:type balancer_method: ``str``
|
|
|
|
:param ex_description: Description of the node (required)
|
|
:type ex_description: ``str``
|
|
|
|
:param health_monitors: A list of health monitors to use for the pool.
|
|
:type health_monitors: ``list`` of
|
|
:class:`DimensionDataDefaultHealthMonitor`
|
|
|
|
:param service_down_action: What to do when node
|
|
is unavailable NONE, DROP or RESELECT
|
|
:type service_down_action: ``str``
|
|
|
|
:param slow_ramp_time: Number of seconds to stagger ramp up of nodes
|
|
:type slow_ramp_time: ``int``
|
|
|
|
:return: Instance of ``DimensionDataPool``
|
|
:rtype: ``DimensionDataPool``
|
|
"""
|
|
# Names cannot contain spaces.
|
|
name.replace(' ', '_')
|
|
create_node_elm = ET.Element('createPool', {'xmlns': TYPES_URN})
|
|
ET.SubElement(create_node_elm, "networkDomainId") \
|
|
.text = network_domain_id
|
|
ET.SubElement(create_node_elm, "name").text = name
|
|
ET.SubElement(create_node_elm, "description").text \
|
|
= str(ex_description)
|
|
ET.SubElement(create_node_elm, "loadBalanceMethod") \
|
|
.text = str(balancer_method)
|
|
|
|
if health_monitors is not None:
|
|
for monitor in health_monitors:
|
|
ET.SubElement(create_node_elm, "healthMonitorId") \
|
|
.text = str(monitor.id)
|
|
|
|
ET.SubElement(create_node_elm, "serviceDownAction") \
|
|
.text = service_down_action
|
|
ET.SubElement(create_node_elm, "slowRampTime").text \
|
|
= str(slow_ramp_time)
|
|
|
|
response = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/createPool',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
|
|
pool_id = None
|
|
for info in findall(response, 'info', TYPES_URN):
|
|
if info.get('name') == 'poolId':
|
|
pool_id = info.get('value')
|
|
|
|
return DimensionDataPool(
|
|
id=pool_id,
|
|
name=name,
|
|
description=ex_description,
|
|
status=State.RUNNING,
|
|
load_balance_method=str(balancer_method),
|
|
health_monitor_id=None,
|
|
service_down_action=service_down_action,
|
|
slow_ramp_time=str(slow_ramp_time)
|
|
)
|
|
|
|
def ex_create_virtual_listener(self,
|
|
network_domain_id,
|
|
name,
|
|
ex_description,
|
|
port=None,
|
|
pool=None,
|
|
listener_ip_address=None,
|
|
persistence_profile=None,
|
|
fallback_persistence_profile=None,
|
|
irule=None,
|
|
protocol='TCP',
|
|
connection_limit=25000,
|
|
connection_rate_limit=2000,
|
|
source_port_preservation='PRESERVE'):
|
|
"""
|
|
Create a new virtual listener (load balancer)
|
|
|
|
:param network_domain_id: Network Domain ID (required)
|
|
:type name: ``str``
|
|
|
|
:param name: name of the listener (required)
|
|
:type name: ``str``
|
|
|
|
:param ex_description: Description of the node (required)
|
|
:type ex_description: ``str``
|
|
|
|
:param port: An integer in the range of 1-65535. If not supplied,
|
|
it will be taken to mean 'Any Port'
|
|
:type port: ``int``
|
|
|
|
:param pool: The pool to use for the listener
|
|
:type pool: :class:`DimensionDataPool`
|
|
|
|
:param listener_ip_address: The IPv4 Address of the virtual listener
|
|
:type listener_ip_address: ``str``
|
|
|
|
:param persistence_profile: Persistence profile
|
|
:type persistence_profile: :class:`DimensionDataPersistenceProfile`
|
|
|
|
:param fallback_persistence_profile: Fallback persistence profile
|
|
:type fallback_persistence_profile:
|
|
:class:`DimensionDataPersistenceProfile`
|
|
|
|
:param irule: The iRule to apply
|
|
:type irule: :class:`DimensionDataDefaultiRule`
|
|
|
|
:param protocol: For STANDARD type, ANY, TCP or UDP
|
|
for PERFORMANCE_LAYER_4 choice of ANY, TCP, UDP, HTTP
|
|
:type protcol: ``str``
|
|
|
|
:param connection_limit: Maximum number
|
|
of concurrent connections per sec
|
|
:type connection_limit: ``int``
|
|
|
|
:param connection_rate_limit: Maximum number of concurrent sessions
|
|
:type connection_rate_limit: ``int``
|
|
|
|
:param source_port_preservation: Choice of PRESERVE,
|
|
PRESERVE_STRICT or CHANGE
|
|
:type source_port_preservation: ``str``
|
|
|
|
:return: Instance of the listener
|
|
:rtype: ``DimensionDataVirtualListener``
|
|
"""
|
|
if (port == 80) or (port == 443):
|
|
listener_type = 'PERFORMANCE_LAYER_4'
|
|
else:
|
|
listener_type = 'STANDARD'
|
|
|
|
create_node_elm = ET.Element('createVirtualListener',
|
|
{'xmlns': TYPES_URN})
|
|
ET.SubElement(create_node_elm, "networkDomainId") \
|
|
.text = network_domain_id
|
|
ET.SubElement(create_node_elm, "name").text = name
|
|
ET.SubElement(create_node_elm, "description").text = \
|
|
str(ex_description)
|
|
ET.SubElement(create_node_elm, "type").text = listener_type
|
|
ET.SubElement(create_node_elm, "protocol") \
|
|
.text = protocol
|
|
if listener_ip_address is not None:
|
|
ET.SubElement(create_node_elm, "listenerIpAddress").text = \
|
|
str(listener_ip_address)
|
|
if port is not None:
|
|
ET.SubElement(create_node_elm, "port").text = str(port)
|
|
ET.SubElement(create_node_elm, "enabled").text = 'true'
|
|
ET.SubElement(create_node_elm, "connectionLimit") \
|
|
.text = str(connection_limit)
|
|
ET.SubElement(create_node_elm, "connectionRateLimit") \
|
|
.text = str(connection_rate_limit)
|
|
ET.SubElement(create_node_elm, "sourcePortPreservation") \
|
|
.text = source_port_preservation
|
|
if pool is not None:
|
|
ET.SubElement(create_node_elm, "poolId") \
|
|
.text = pool.id
|
|
if persistence_profile is not None:
|
|
ET.SubElement(create_node_elm, "persistenceProfileId") \
|
|
.text = persistence_profile.id
|
|
if fallback_persistence_profile is not None:
|
|
ET.SubElement(create_node_elm, "fallbackPersistenceProfileId") \
|
|
.text = fallback_persistence_profile.id
|
|
if irule is not None:
|
|
ET.SubElement(create_node_elm, "iruleId") \
|
|
.text = irule.id
|
|
|
|
response = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/createVirtualListener',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
|
|
virtual_listener_id = None
|
|
virtual_listener_ip = None
|
|
for info in findall(response, 'info', TYPES_URN):
|
|
if info.get('name') == 'virtualListenerId':
|
|
virtual_listener_id = info.get('value')
|
|
if info.get('name') == 'listenerIpAddress':
|
|
virtual_listener_ip = info.get('value')
|
|
|
|
return DimensionDataVirtualListener(
|
|
id=virtual_listener_id,
|
|
name=name,
|
|
ip=virtual_listener_ip,
|
|
status=State.RUNNING
|
|
)
|
|
|
|
def ex_get_pools(self, ex_network_domain_id=None):
|
|
"""
|
|
Get all of the pools inside the current geography or
|
|
in given network.
|
|
|
|
:param ex_network_domain_id: UUID of Network Domain
|
|
if not None returns only balancers in the given network
|
|
if None then returns all pools for the organization
|
|
:type ex_network_domain_id: ``str``
|
|
|
|
:return: Returns a ``list`` of type ``DimensionDataPool``
|
|
:rtype: ``list`` of ``DimensionDataPool``
|
|
"""
|
|
params = None
|
|
|
|
if ex_network_domain_id is not None:
|
|
params = {"networkDomainId": ex_network_domain_id}
|
|
|
|
pools = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/pool',
|
|
params=params).object
|
|
|
|
return self._to_pools(pools)
|
|
|
|
def ex_get_pool(self, pool_id):
|
|
"""
|
|
Get a specific pool inside the current geography
|
|
|
|
:param pool_id: The identifier of the pool
|
|
:type pool_id: ``str``
|
|
|
|
:return: Returns an instance of ``DimensionDataPool``
|
|
:rtype: ``DimensionDataPool``
|
|
"""
|
|
pool = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/pool/%s'
|
|
% pool_id).object
|
|
return self._to_pool(pool)
|
|
|
|
def ex_update_pool(self, pool):
|
|
"""
|
|
Update the properties of an existing pool
|
|
only method, serviceDownAction and slowRampTime are updated
|
|
|
|
:param pool: The instance of ``DimensionDataPool`` to update
|
|
:type pool: ``DimensionDataPool``
|
|
|
|
:return: ``True`` for success, ``False`` for failure
|
|
:rtype: ``bool``
|
|
"""
|
|
create_node_elm = ET.Element('editPool', {'xmlns': TYPES_URN})
|
|
|
|
ET.SubElement(create_node_elm, "loadBalanceMethod") \
|
|
.text = str(pool.load_balance_method)
|
|
ET.SubElement(create_node_elm, "serviceDownAction") \
|
|
.text = pool.service_down_action
|
|
ET.SubElement(create_node_elm, "slowRampTime").text \
|
|
= str(pool.slow_ramp_time)
|
|
|
|
response = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/editPool',
|
|
method='POST',
|
|
data=ET.tostring(create_node_elm)).object
|
|
response_code = findtext(response, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_destroy_pool(self, pool):
|
|
"""
|
|
Destroy an existing pool
|
|
|
|
:param pool: The instance of ``DimensionDataPool`` to destroy
|
|
:type pool: ``DimensionDataPool``
|
|
|
|
:return: ``True`` for success, ``False`` for failure
|
|
:rtype: ``bool``
|
|
"""
|
|
destroy_request = ET.Element('deletePool',
|
|
{'xmlns': TYPES_URN,
|
|
'id': pool.id})
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/deletePool',
|
|
method='POST',
|
|
data=ET.tostring(destroy_request)).object
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_get_pool_members(self, pool_id):
|
|
"""
|
|
Get the members of a pool
|
|
|
|
:param pool: The instance of a pool
|
|
:type pool: ``DimensionDataPool``
|
|
|
|
:return: Returns an ``list`` of ``DimensionDataPoolMember``
|
|
:rtype: ``list`` of ``DimensionDataPoolMember``
|
|
"""
|
|
members = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/poolMember?poolId=%s'
|
|
% pool_id).object
|
|
return self._to_members(members)
|
|
|
|
def ex_get_pool_member(self, pool_member_id):
|
|
"""
|
|
Get a specific member of a pool
|
|
|
|
:param pool: The id of a pool member
|
|
:type pool: ``str``
|
|
|
|
:return: Returns an instance of ``DimensionDataPoolMember``
|
|
:rtype: ``DimensionDataPoolMember``
|
|
"""
|
|
member = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/poolMember/%s'
|
|
% pool_member_id).object
|
|
return self._to_member(member)
|
|
|
|
def ex_set_pool_member_state(self, member, enabled=True):
|
|
request = ET.Element('editPoolMember',
|
|
{'xmlns': TYPES_URN,
|
|
'id': member.id})
|
|
state = "ENABLED" if enabled is True else "DISABLED"
|
|
ET.SubElement(request, 'status').text = state
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/editPoolMember',
|
|
method='POST',
|
|
data=ET.tostring(request)).object
|
|
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_destroy_pool_member(self, member, destroy_node=False):
|
|
"""
|
|
Destroy a specific member of a pool
|
|
|
|
:param pool: The instance of a pool member
|
|
:type pool: ``DimensionDataPoolMember``
|
|
|
|
:param destroy_node: Also destroy the associated node
|
|
:type destroy_node: ``bool``
|
|
|
|
:return: ``True`` for success, ``False`` for failure
|
|
:rtype: ``bool``
|
|
"""
|
|
# remove the pool member
|
|
destroy_request = ET.Element('removePoolMember',
|
|
{'xmlns': TYPES_URN,
|
|
'id': member.id})
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/removePoolMember',
|
|
method='POST',
|
|
data=ET.tostring(destroy_request)).object
|
|
|
|
if member.node_id is not None and destroy_node is True:
|
|
return self.ex_destroy_node(member.node_id)
|
|
else:
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_get_nodes(self, ex_network_domain_id=None):
|
|
"""
|
|
Get the nodes within this geography or in given network.
|
|
|
|
:param ex_network_domain_id: UUID of Network Domain
|
|
if not None returns only balancers in the given network
|
|
if None then returns all pools for the organization
|
|
:type ex_network_domain_id: ``str``
|
|
|
|
:return: Returns an ``list`` of ``DimensionDataVIPNode``
|
|
:rtype: ``list`` of ``DimensionDataVIPNode``
|
|
"""
|
|
params = None
|
|
if ex_network_domain_id is not None:
|
|
params = {"networkDomainId": ex_network_domain_id}
|
|
|
|
nodes = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/node',
|
|
params=params).object
|
|
return self._to_nodes(nodes)
|
|
|
|
def ex_get_node(self, node_id):
|
|
"""
|
|
Get the node specified by node_id
|
|
|
|
:return: Returns an instance of ``DimensionDataVIPNode``
|
|
:rtype: Instance of ``DimensionDataVIPNode``
|
|
"""
|
|
nodes = self.connection \
|
|
.request_with_orgId_api_2('networkDomainVip/node/%s'
|
|
% node_id).object
|
|
return self._to_node(nodes)
|
|
|
|
def ex_destroy_node(self, node_id):
|
|
"""
|
|
Destroy a specific node
|
|
|
|
:param node_id: The ID of of a ``DimensionDataVIPNode``
|
|
:type node_id: ``str``
|
|
|
|
:return: ``True`` for success, ``False`` for failure
|
|
:rtype: ``bool``
|
|
"""
|
|
# Destroy the node
|
|
destroy_request = ET.Element('deleteNode',
|
|
{'xmlns': TYPES_URN,
|
|
'id': node_id})
|
|
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/deleteNode',
|
|
method='POST',
|
|
data=ET.tostring(destroy_request)).object
|
|
response_code = findtext(result, 'responseCode', TYPES_URN)
|
|
return response_code in ['IN_PROGRESS', 'OK']
|
|
|
|
def ex_wait_for_state(self, state, func, poll_interval=2,
|
|
timeout=60, *args, **kwargs):
|
|
"""
|
|
Wait for the function which returns a instance
|
|
with field status to match
|
|
|
|
Keep polling func until one of the desired states is matched
|
|
|
|
:param state: Either the desired state (`str`) or a `list` of states
|
|
:type state: ``str`` or ``list``
|
|
|
|
:param func: The function to call, e.g. ex_get_vlan
|
|
:type func: ``function``
|
|
|
|
:param poll_interval: The number of seconds to wait between checks
|
|
:type poll_interval: `int`
|
|
|
|
:param timeout: The total number of seconds to wait to reach a state
|
|
:type timeout: `int`
|
|
|
|
:param args: The arguments for func
|
|
:type args: Positional arguments
|
|
|
|
:param kwargs: The arguments for func
|
|
:type kwargs: Keyword arguments
|
|
"""
|
|
return self.connection.wait_for_state(state, func, poll_interval,
|
|
timeout, *args, **kwargs)
|
|
|
|
def ex_get_default_health_monitors(self, network_domain_id):
|
|
"""
|
|
Get the default health monitors available for a network domain
|
|
|
|
:param network_domain_id: The ID of of a ``DimensionDataNetworkDomain``
|
|
:type network_domain_id: ``str``
|
|
|
|
:rtype: `list` of :class:`DimensionDataDefaultHealthMonitor`
|
|
"""
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/defaultHealthMonitor',
|
|
params={'networkDomainId': network_domain_id},
|
|
method='GET').object
|
|
return self._to_health_monitors(result)
|
|
|
|
def ex_get_default_persistence_profiles(self, network_domain_id):
|
|
"""
|
|
Get the default persistence profiles available for a network domain
|
|
|
|
:param network_domain_id: The ID of of a ``DimensionDataNetworkDomain``
|
|
:type network_domain_id: ``str``
|
|
|
|
:rtype: `list` of :class:`DimensionDataPersistenceProfile`
|
|
"""
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/defaultPersistenceProfile',
|
|
params={'networkDomainId': network_domain_id},
|
|
method='GET').object
|
|
return self._to_persistence_profiles(result)
|
|
|
|
def ex_get_default_irules(self, network_domain_id):
|
|
"""
|
|
Get the default iRules available for a network domain
|
|
|
|
:param network_domain_id: The ID of of a ``DimensionDataNetworkDomain``
|
|
:type network_domain_id: ``str``
|
|
|
|
:rtype: `list` of :class:`DimensionDataDefaultiRule`
|
|
"""
|
|
result = self.connection.request_with_orgId_api_2(
|
|
action='networkDomainVip/defaultIrule',
|
|
params={'networkDomainId': network_domain_id},
|
|
method='GET').object
|
|
return self._to_irules(result)
|
|
|
|
def _to_irules(self, object):
|
|
irules = []
|
|
matches = object.findall(
|
|
fixxpath('defaultIrule', TYPES_URN))
|
|
for element in matches:
|
|
irules.append(self._to_irule(element))
|
|
return irules
|
|
|
|
def _to_irule(self, element):
|
|
compatible = []
|
|
matches = element.findall(
|
|
fixxpath('virtualListenerCompatibility', TYPES_URN))
|
|
for match_element in matches:
|
|
compatible.append(
|
|
DimensionDataVirtualListenerCompatibility(
|
|
type=match_element.get('type'),
|
|
protocol=match_element.get('protocol', None)))
|
|
irule_element = element.find(fixxpath('irule', TYPES_URN))
|
|
return DimensionDataDefaultiRule(
|
|
id=irule_element.get('id'),
|
|
name=irule_element.get('name'),
|
|
compatible_listeners=compatible
|
|
)
|
|
|
|
def _to_persistence_profiles(self, object):
|
|
profiles = []
|
|
matches = object.findall(
|
|
fixxpath('defaultPersistenceProfile', TYPES_URN))
|
|
for element in matches:
|
|
profiles.append(self._to_persistence_profile(element))
|
|
return profiles
|
|
|
|
def _to_persistence_profile(self, element):
|
|
compatible = []
|
|
matches = element.findall(
|
|
fixxpath('virtualListenerCompatibility', TYPES_URN))
|
|
for match_element in matches:
|
|
compatible.append(
|
|
DimensionDataVirtualListenerCompatibility(
|
|
type=match_element.get('type'),
|
|
protocol=match_element.get('protocol', None)))
|
|
|
|
return DimensionDataPersistenceProfile(
|
|
id=element.get('id'),
|
|
fallback_compatible=bool(
|
|
element.get('fallbackCompatible') == "true"),
|
|
name=findtext(element, 'name', TYPES_URN),
|
|
compatible_listeners=compatible
|
|
)
|
|
|
|
def _to_health_monitors(self, object):
|
|
monitors = []
|
|
matches = object.findall(fixxpath('defaultHealthMonitor', TYPES_URN))
|
|
for element in matches:
|
|
monitors.append(self._to_health_monitor(element))
|
|
return monitors
|
|
|
|
def _to_health_monitor(self, element):
|
|
return DimensionDataDefaultHealthMonitor(
|
|
id=element.get('id'),
|
|
name=findtext(element, 'name', TYPES_URN),
|
|
node_compatible=bool(
|
|
findtext(element, 'nodeCompatible', TYPES_URN) == "true"),
|
|
pool_compatible=bool(
|
|
findtext(element, 'poolCompatible', TYPES_URN) == "true"),
|
|
)
|
|
|
|
def _to_nodes(self, object):
|
|
nodes = []
|
|
for element in object.findall(fixxpath("node", TYPES_URN)):
|
|
nodes.append(self._to_node(element))
|
|
|
|
return nodes
|
|
|
|
def _to_node(self, element):
|
|
ipaddress = findtext(element, 'ipv4Address', TYPES_URN)
|
|
if ipaddress is None:
|
|
ipaddress = findtext(element, 'ipv6Address', TYPES_URN)
|
|
|
|
name = findtext(element, 'name', TYPES_URN)
|
|
|
|
node = DimensionDataVIPNode(
|
|
id=element.get('id'),
|
|
name=name,
|
|
status=self._VALUE_TO_STATE_MAP.get(
|
|
findtext(element, 'state', TYPES_URN),
|
|
State.UNKNOWN),
|
|
connection_rate_limit=findtext(element,
|
|
'connectionRateLimit', TYPES_URN),
|
|
connection_limit=findtext(element, 'connectionLimit', TYPES_URN),
|
|
ip=ipaddress)
|
|
|
|
return node
|
|
|
|
def _to_balancers(self, object):
|
|
loadbalancers = []
|
|
for element in object.findall(fixxpath("virtualListener", TYPES_URN)):
|
|
loadbalancers.append(self._to_balancer(element))
|
|
|
|
return loadbalancers
|
|
|
|
def _to_balancer(self, element):
|
|
ipaddress = findtext(element, 'listenerIpAddress', TYPES_URN)
|
|
name = findtext(element, 'name', TYPES_URN)
|
|
port = findtext(element, 'port', TYPES_URN)
|
|
extra = {}
|
|
|
|
pool_element = element.find(fixxpath(
|
|
'pool',
|
|
TYPES_URN))
|
|
if pool_element is None:
|
|
extra['pool_id'] = None
|
|
|
|
else:
|
|
extra['pool_id'] = pool_element.get('id')
|
|
|
|
extra['network_domain_id'] = findtext(element, 'networkDomainId',
|
|
TYPES_URN)
|
|
|
|
balancer = LoadBalancer(
|
|
id=element.get('id'),
|
|
name=name,
|
|
state=self._VALUE_TO_STATE_MAP.get(
|
|
findtext(element, 'state', TYPES_URN),
|
|
State.UNKNOWN),
|
|
ip=ipaddress,
|
|
port=port,
|
|
driver=self.connection.driver,
|
|
extra=extra
|
|
)
|
|
|
|
return balancer
|
|
|
|
def _to_members(self, object):
|
|
members = []
|
|
for element in object.findall(fixxpath("poolMember", TYPES_URN)):
|
|
members.append(self._to_member(element))
|
|
|
|
return members
|
|
|
|
def _to_member(self, element):
|
|
port = findtext(element, 'port', TYPES_URN)
|
|
if port is not None:
|
|
port = int(port)
|
|
pool_member = DimensionDataPoolMember(
|
|
id=element.get('id'),
|
|
name=element.find(fixxpath(
|
|
'node',
|
|
TYPES_URN)).get('name'),
|
|
status=findtext(element, 'state', TYPES_URN),
|
|
node_id=element.find(fixxpath(
|
|
'node',
|
|
TYPES_URN)).get('id'),
|
|
ip=element.find(fixxpath(
|
|
'node',
|
|
TYPES_URN)).get('ipAddress'),
|
|
port=port
|
|
)
|
|
return pool_member
|
|
|
|
def _to_pools(self, object):
|
|
pools = []
|
|
for element in object.findall(fixxpath("pool", TYPES_URN)):
|
|
pools.append(self._to_pool(element))
|
|
|
|
return pools
|
|
|
|
def _to_pool(self, element):
|
|
pool = DimensionDataPool(
|
|
id=element.get('id'),
|
|
name=findtext(element, 'name', TYPES_URN),
|
|
status=findtext(element, 'state', TYPES_URN),
|
|
description=findtext(element, 'description', TYPES_URN),
|
|
load_balance_method=findtext(element, 'loadBalanceMethod',
|
|
TYPES_URN),
|
|
health_monitor_id=findtext(element, 'healthMonitorId', TYPES_URN),
|
|
service_down_action=findtext(element, 'serviceDownAction',
|
|
TYPES_URN),
|
|
slow_ramp_time=findtext(element, 'slowRampTime', TYPES_URN),
|
|
)
|
|
return pool
|