# 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 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__ = ["SLB_API_VERSION", "SLBDriver"]
try:
import simplejson as json
except ImportError:
import json
from libcloud.utils.py3 import u
from libcloud.utils.xml import findall, findattr, findtext
from libcloud.utils.misc import ReprMixin
from libcloud.common.types import LibcloudError
from libcloud.common.aliyun import AliyunXmlResponse, SignedAliyunConnection
from libcloud.loadbalancer.base import Driver, Member, Algorithm, LoadBalancer
from libcloud.loadbalancer.types import State
SLB_API_VERSION = "2014-05-15"
SLB_API_HOST = "slb.aliyuncs.com"
DEFAULT_SIGNATURE_VERSION = "1.0"
STATE_MAPPINGS = {
"inactive": State.UNKNOWN,
"active": State.RUNNING,
"locked": State.PENDING,
}
RESOURCE_EXTRA_ATTRIBUTES_MAP = {
"balancer": {
"create_timestamp": {"xpath": "CreateTimeStamp", "transform_func": int},
"address_type": {"xpath": "AddressType", "transform_func": u},
"region_id": {"xpath": "RegionId", "transform_func": u},
"region_id_alias": {"xpath": "RegionIdAlias", "transform_func": u},
"create_time": {"xpath": "CreateTime", "transform_func": u},
"master_zone_id": {"xpath": "MasterZoneId", "transform_func": u},
"slave_zone_id": {"xpath": "SlaveZoneId", "transform_func": u},
"network_type": {"xpath": "NetworkType", "transform_func": u},
}
}
SLB_SCHEDULER_TO_ALGORITHM = {
"wrr": Algorithm.WEIGHTED_ROUND_ROBIN,
"wlc": Algorithm.WEIGHTED_LEAST_CONNECTIONS,
}
ALGORITHM_TO_SLB_SCHEDULER = {
Algorithm.WEIGHTED_ROUND_ROBIN: "wrr",
Algorithm.WEIGHTED_LEAST_CONNECTIONS: "wlc",
}
class SLBConnection(SignedAliyunConnection):
api_version = SLB_API_VERSION
host = SLB_API_HOST
responseCls = AliyunXmlResponse
service_name = "slb"
class SLBLoadBalancerAttribute:
"""
This class used to get listeners and backend servers related to a balancer
listeners is a ``list`` of ``dict``, each element contains
'ListenerPort' and 'ListenerProtocol' keys.
backend_servers is a ``list`` of ``dict``, each element contains
'ServerId' and 'Weight' keys.
"""
def __init__(self, balancer, listeners, backend_servers, extra=None):
self.balancer = balancer
self.listeners = listeners or []
self.backend_servers = backend_servers or []
self.extra = extra or {}
def is_listening(self, port):
for listener in self.listeners:
if listener.get("ListenerPort") == port:
return True
return False
def is_attached(self, member):
for server in self.backend_servers:
if server.get("Serverid") == member.id:
return True
return False
def __repr__(self):
return "<SLBLoadBalancerAttribute id={}, ports={}, servers={} ...>".format(
self.balancer.id,
self.listeners,
self.backend_servers,
)
class SLBLoadBalancerListener(ReprMixin):
"""
Base SLB load balancer listener class
"""
_repr_attributes = ["port", "backend_port", "scheduler", "bandwidth"]
action = None
option_keys = []
def __init__(self, port, backend_port, algorithm, bandwidth, extra=None):
self.port = port
self.backend_port = backend_port
self.scheduler = ALGORITHM_TO_SLB_SCHEDULER.get(algorithm, "wrr")
self.bandwidth = bandwidth
self.extra = extra or {}
@classmethod
def create(cls, port, backend_port, algorithm, bandwidth, extra=None):
return cls(port, backend_port, algorithm, bandwidth, extra=extra)
def get_create_params(self):
params = self.get_required_params()
options = self.get_optional_params()
options.update(params)
return options
def get_required_params(self):
params = {
"Action": self.action,
"ListenerPort": self.port,
"BackendServerPort": self.backend_port,
"Scheduler": self.scheduler,
"Bandwidth": self.bandwidth,
}
return params
def get_optional_params(self):
options = {}
for option in self.option_keys:
if self.extra and option in self.extra:
options[option] = self.extra[option]
return options
class SLBLoadBalancerHttpListener(SLBLoadBalancerListener):
"""
This class represents a rule to route http request to the backends.
"""
action = "CreateLoadBalancerHTTPListener"
option_keys = [
"XForwardedFor",
"StickySessionType",
"CookieTimeout",
"Cookie",
"HealthCheckDomain",
"HealthCheckURI",
"HealthCheckConnectPort",
"HealthyThreshold",
"UnhealthyThreshold",
"HealthCheckTimeout",
"HealthCheckInterval",
"HealthCheckHttpCode",
]
def __init__(
self,
port,
backend_port,
algorithm,
bandwidth,
sticky_session,
health_check,
extra=None,
):
super().__init__(port, backend_port, algorithm, bandwidth, extra=extra)
self.sticky_session = sticky_session
self.health_check = health_check
def get_required_params(self):
params = super().get_required_params()
params["StickySession"] = self.sticky_session
params["HealthCheck"] = self.health_check
return params
@classmethod
def create(cls, port, backend_port, algorithm, bandwidth, extra={}):
if "StickySession" not in extra:
raise AttributeError("StickySession is required")
if "HealthCheck" not in extra:
raise AttributeError("HealthCheck is required")
sticky_session = extra["StickySession"]
health_check = extra["HealthCheck"]
return cls(
port,
backend_port,
algorithm,
bandwidth,
sticky_session,
health_check,
extra=extra,
)
class SLBLoadBalancerHttpsListener(SLBLoadBalancerListener):
"""
This class represents a rule to route https request to the backends.
"""
action = "CreateLoadBalancerHTTPSListener"
option_keys = [
"XForwardedFor",
"StickySessionType",
"CookieTimeout",
"Cookie",
"HealthCheckDomain",
"HealthCheckURI",
"HealthCheckConnectPort",
"HealthyThreshold",
"UnhealthyThreshold",
"HealthCheckTimeout",
"HealthCheckInterval",
"HealthCheckHttpCode",
]
def __init__(
self,
port,
backend_port,
algorithm,
bandwidth,
sticky_session,
health_check,
certificate_id,
extra=None,
):
super().__init__(port, backend_port, algorithm, bandwidth, extra=extra)
self.sticky_session = sticky_session
self.health_check = health_check
self.certificate_id = certificate_id
def get_required_params(self):
params = super().get_required_params()
params["StickySession"] = self.sticky_session
params["HealthCheck"] = self.health_check
params["ServerCertificateId"] = self.certificate_id
return params
@classmethod
def create(cls, port, backend_port, algorithm, bandwidth, extra={}):
if "StickySession" not in extra:
raise AttributeError("StickySession is required")
if "HealthCheck" not in extra:
raise AttributeError("HealthCheck is required")
if "ServerCertificateId" not in extra:
raise AttributeError("ServerCertificateId is required")
sticky_session = extra["StickySession"]
health_check = extra["HealthCheck"]
certificate_id = extra["ServerCertificateId"]
return cls(
port,
backend_port,
algorithm,
bandwidth,
sticky_session,
health_check,
certificate_id,
extra=extra,
)
class SLBLoadBalancerTcpListener(SLBLoadBalancerListener):
"""
This class represents a rule to route tcp request to the backends.
"""
action = "CreateLoadBalancerTCPListener"
option_keys = [
"PersistenceTimeout",
"HealthCheckType",
"HealthCheckDomain",
"HealthCheckURI",
"HealthCheckConnectPort",
"HealthyThreshold",
"UnhealthyThreshold",
"HealthCheckConnectTimeout",
"HealthCheckInterval",
"HealthCheckHttpCode",
]
class SLBLoadBalancerUdpListener(SLBLoadBalancerTcpListener):
"""
This class represents a rule to route udp request to the backends.
"""
action = "CreateLoadBalancerUDPListener"
option_keys = [
"PersistenceTimeout",
"HealthCheckConnectPort",
"HealthyThreshold",
"UnhealthyThreshold",
"HealthCheckConnectTimeout",
"HealthCheckInterval",
]
class SLBServerCertificate(ReprMixin):
_repr_attributes = ["id", "name", "fingerprint"]
def __init__(self, id, name, fingerprint):
self.id = id
self.name = name
self.fingerprint = fingerprint
PROTOCOL_TO_LISTENER_MAP = {
"http": SLBLoadBalancerHttpListener,
"https": SLBLoadBalancerHttpsListener,
"tcp": SLBLoadBalancerTcpListener,
"udp": SLBLoadBalancerUdpListener,
}
[docs]class SLBDriver(Driver):
"""
Aliyun SLB load balancer driver.
"""
name = "Aliyun Server Load Balancer"
website = "https://www.aliyun.com/product/slb"
connectionCls = SLBConnection
path = "/"
namespace = None
_VALUE_TO_ALGORITHM_MAP = SLB_SCHEDULER_TO_ALGORITHM
_ALGORITHM_TO_VALUE_MAP = ALGORITHM_TO_SLB_SCHEDULER
def __init__(self, access_id, secret, region):
super().__init__(access_id, secret)
self.region = region
[docs] def list_protocols(self):
return list(PROTOCOL_TO_LISTENER_MAP.keys())
[docs] def list_balancers(self, ex_balancer_ids=None, ex_filters=None):
"""
List all loadbalancers
@inherits :class:`Driver.list_balancers`
:keyword ex_balancer_ids: a list of balancer ids to filter results
Only balancers which's id in this list
will be returned
:type ex_balancer_ids: ``list`` of ``str``
:keyword ex_filters: attributes to filter results. Only balancers
which have all the desired attributes
and values will be returned
:type ex_filters: ``dict``
"""
params = {"Action": "DescribeLoadBalancers", "RegionId": self.region}
if ex_balancer_ids and isinstance(ex_balancer_ids, list):
params["LoadBalancerId"] = ",".join(ex_balancer_ids)
if ex_filters and isinstance(ex_filters, dict):
ex_filters.update(params)
params = ex_filters
resp_body = self.connection.request(self.path, params=params).object
return self._to_balancers(resp_body)
[docs] def create_balancer(
self,
name,
port,
protocol,
algorithm,
members,
ex_bandwidth=None,
ex_internet_charge_type=None,
ex_address_type=None,
ex_vswitch_id=None,
ex_master_zone_id=None,
ex_slave_zone_id=None,
ex_client_token=None,
**kwargs,
):
"""
Create a new load balancer instance
@inherits: :class:`Driver.create_balancer`
:keyword ex_bandwidth: The max bandwidth limit for `paybybandwidth`
internet charge type, in Mbps unit
:type ex_bandwidth: ``int`` in range [1, 1000]
:keyword ex_internet_charge_type: The internet charge type
:type ex_internet_charge_type: a ``str`` of `paybybandwidth`
or `paybytraffic`
:keyword ex_address_type: The listening IP address type
:type ex_address_type: a ``str`` of `internet` or `intranet`
:keyword ex_vswitch_id: The vswitch id in a VPC network
:type ex_vswitch_id: ``str``
:keyword ex_master_zone_id: The id of the master availability zone
:type ex_master_zone_id: ``str``
:keyword ex_slave_zone_id: The id of the slave availability zone
:type ex_slave_zone_id: ``str``
:keyword ex_client_token: The token generated by client to
identify requests
:type ex_client_token: ``str``
"""
# 1.Create load balancer
params = {"Action": "CreateLoadBalancer", "RegionId": self.region}
if name:
params["LoadBalancerName"] = name
if not port:
raise AttributeError("port is required")
if not protocol:
# NOTE(samsong8610): Use http listener as default
protocol = "http"
if protocol not in PROTOCOL_TO_LISTENER_MAP:
raise AttributeError("unsupported protocol %s" % protocol)
# Bandwidth in range [1, 1000] Mbps
bandwidth = -1
if ex_bandwidth:
try:
bandwidth = int(ex_bandwidth)
except ValueError:
raise AttributeError("ex_bandwidth should be a integer in " "range [1, 1000].")
params["Bandwidth"] = bandwidth
if ex_internet_charge_type:
if ex_internet_charge_type.lower() == "paybybandwidth":
if bandwidth == -1:
raise AttributeError(
"PayByBandwidth internet charge type" " need ex_bandwidth be set"
)
params["InternetChargeType"] = ex_internet_charge_type
if ex_address_type:
if ex_address_type.lower() not in ("internet", "intranet"):
raise AttributeError('ex_address_type should be "internet" ' 'or "intranet"')
params["AddressType"] = ex_address_type
if ex_vswitch_id:
params["VSwitchId"] = ex_vswitch_id
if ex_master_zone_id:
params["MasterZoneId"] = ex_master_zone_id
if ex_slave_zone_id:
params["SlaveZoneId"] = ex_slave_zone_id
if ex_client_token:
params["ClientToken"] = ex_client_token
if members and isinstance(members, list):
backend_ports = [member.port for member in members]
if len(set(backend_ports)) != 1:
raise AttributeError("the ports of members should be unique")
# NOTE(samsong8610): If members do not provide backend port,
# default to listening port
backend_port = backend_ports[0] or port
else:
backend_port = port
balancer = None
try:
resp_body = self.connection.request(self.path, params).object
balancer = self._to_balancer(resp_body)
balancer.port = port
# 2.Add backend servers
if members is None:
members = []
for member in members:
self.balancer_attach_member(balancer, member)
# 3.Create listener
# NOTE(samsong8610): Assume only create a listener which uses all
# the bandwidth.
self.ex_create_listener(
balancer, backend_port, protocol, algorithm, bandwidth, **kwargs
)
self.ex_start_listener(balancer, port)
return balancer
except Exception as e:
if balancer is not None:
try:
self.destroy_balancer(balancer)
except Exception:
pass
raise e
[docs] def destroy_balancer(self, balancer):
params = {"Action": "DeleteLoadBalancer", "LoadBalancerId": balancer.id}
resp = self.connection.request(self.path, params)
return resp.success()
[docs] def get_balancer(self, balancer_id):
balancers = self.list_balancers(ex_balancer_ids=[balancer_id])
if len(balancers) != 1:
raise LibcloudError("could not find load balancer with id %s" % balancer_id)
return balancers[0]
[docs] def balancer_attach_compute_node(self, balancer, node):
if len(node.public_ips) > 0:
ip = node.public_ips[0]
else:
ip = node.private_ips[0]
member = Member(id=node.id, ip=ip, port=balancer.port)
return self.balancer_attach_member(balancer, member)
[docs] def balancer_attach_member(self, balancer, member):
params = {"Action": "AddBackendServers", "LoadBalancerId": balancer.id}
if member and isinstance(member, Member):
params["BackendServers"] = self._to_servers_json([member])
self.connection.request(self.path, params)
return member
[docs] def balancer_detach_member(self, balancer, member):
params = {"Action": "RemoveBackendServers", "LoadBalancerId": balancer.id}
if member and isinstance(member, Member):
params["BackendServers"] = self._list_to_json([member.id])
self.connection.request(self.path, params)
return member
[docs] def balancer_list_members(self, balancer):
attribute = self.ex_get_balancer_attribute(balancer)
members = [
Member(
server["ServerId"],
None,
None,
balancer=balancer,
extra={"Weight": server["Weight"]},
)
for server in attribute.backend_servers
]
return members
[docs] def ex_get_balancer_attribute(self, balancer):
"""
Get balancer attribute
:param balancer: the balancer to get attribute
:type balancer: ``LoadBalancer``
:return: the balancer attribute
:rtype: ``SLBLoadBalancerAttribute``
"""
params = {
"Action": "DescribeLoadBalancerAttribute",
"LoadBalancerId": balancer.id,
}
resp_body = self.connection.request(self.path, params).object
attribute = self._to_balancer_attribute(resp_body)
return attribute
[docs] def ex_list_listeners(self, balancer):
"""
Get all listener related to the given balancer
:param balancer: the balancer to list listeners
:type balancer: ``LoadBalancer``
:return: a list of listeners
:rtype: ``list`` of ``SLBLoadBalancerListener``
"""
attribute = self.ex_get_balancer_attribute(balancer)
listeners = [
SLBLoadBalancerListener(each["ListenerPort"], None, None, None)
for each in attribute.listeners
]
return listeners
[docs] def ex_create_listener(self, balancer, backend_port, protocol, algorithm, bandwidth, **kwargs):
"""
Create load balancer listening rule.
:param balancer: the balancer which the rule belongs to.
The listener created will listen on the port of the
the balancer as default. 'ListenerPort' in kwargs
will *OVERRIDE* it.
:type balancer: ``LoadBalancer``
:param backend_port: the backend server port
:type backend_port: ``int``
:param protocol: the balancer protocol, default to http
:type protocol: ``str``
:param algorithm: the balancer routing algorithm
:type algorithm: ``Algorithm``
:param bandwidth: the listener bandwidth limits
:type bandwidth: ``str``
:return: the created listener
:rtype: ``SLBLoadBalancerListener``
"""
cls = PROTOCOL_TO_LISTENER_MAP.get(protocol, SLBLoadBalancerHttpListener)
if "ListenerPort" in kwargs:
port = kwargs["ListenerPort"]
else:
port = balancer.port
listener = cls.create(port, backend_port, algorithm, bandwidth, extra=kwargs)
params = listener.get_create_params()
params["LoadBalancerId"] = balancer.id
params["RegionId"] = self.region
resp = self.connection.request(self.path, params)
return resp.success()
[docs] def ex_start_listener(self, balancer, port):
"""
Start balancer's listener listening the given port.
:param balancer: a load balancer
:type balancer: ``LoadBalancer``
:param port: listening port
:type port: ``int``
:return: whether operation is success
:rtype: ``bool``
"""
params = {
"Action": "StartLoadBalancerListener",
"LoadBalancerId": balancer.id,
"ListenerPort": port,
}
resp = self.connection.request(self.path, params)
return resp.success()
[docs] def ex_stop_listener(self, balancer, port):
"""
Stop balancer's listener listening the given port.
:param balancer: a load balancer
:type balancer: ``LoadBalancer``
:param port: listening port
:type port: ``int``
:return: whether operation is success
:rtype: ``bool``
"""
params = {
"Action": "StopLoadBalancerListener",
"LoadBalancerId": balancer.id,
"ListenerPort": port,
}
resp = self.connection.request(self.path, params)
return resp.success()
[docs] def ex_upload_certificate(self, name, server_certificate, private_key):
"""
Upload certificate and private key for https load balancer listener
:param name: the certificate name
:type name: ``str``
:param server_certificate: the content of the certificate to upload
in PEM format
:type server_certificate: ``str``
:param private_key: the content of the private key to upload
in PEM format
:type private_key: ``str``
:return: new created certificate info
:rtype: ``SLBServerCertificate``
"""
params = {
"Action": "UploadServerCertificate",
"RegionId": self.region,
"ServerCertificate": server_certificate,
"PrivateKey": private_key,
}
if name:
params["ServerCertificateName"] = name
resp_body = self.connection.request(self.path, params).object
return self._to_server_certificate(resp_body)
[docs] def ex_list_certificates(self, certificate_ids=[]):
"""
List all server certificates
:param certificate_ids: certificate ids to filter results
:type certificate_ids: ``str``
:return: certificates
:rtype: ``SLBServerCertificate``
"""
params = {"Action": "DescribeServerCertificates", "RegionId": self.region}
if certificate_ids and isinstance(certificate_ids, list):
params["ServerCertificateId"] = ",".join(certificate_ids)
resp_body = self.connection.request(self.path, params).object
cert_elements = findall(
resp_body, "ServerCertificates/ServerCertificate", namespace=self.namespace
)
certificates = [self._to_server_certificate(el) for el in cert_elements]
return certificates
[docs] def ex_delete_certificate(self, certificate_id):
"""
Delete the given server certificate
:param certificate_id: the id of the certificate to delete
:type certificate_id: ``str``
:return: whether process is success
:rtype: ``bool``
"""
params = {
"Action": "DeleteServerCertificate",
"RegionId": self.region,
"ServerCertificateId": certificate_id,
}
resp = self.connection.request(self.path, params)
return resp.success()
[docs] def ex_set_certificate_name(self, certificate_id, name):
"""
Set server certificate name.
:param certificate_id: the id of the server certificate to update
:type certificate_id: ``str``
:param name: the new name
:type name: ``str``
:return: whether updating is success
:rtype: ``bool``
"""
params = {
"Action": "SetServerCertificateName",
"RegionId": self.region,
"ServerCertificateId": certificate_id,
"ServerCertificateName": name,
}
resp = self.connection.request(self.path, params)
return resp.success()
def _to_balancers(self, element):
xpath = "LoadBalancers/LoadBalancer"
return [
self._to_balancer(el)
for el in findall(element=element, xpath=xpath, namespace=self.namespace)
]
def _to_balancer(self, el):
_id = findtext(element=el, xpath="LoadBalancerId", namespace=self.namespace)
name = findtext(element=el, xpath="LoadBalancerName", namespace=self.namespace)
status = findtext(element=el, xpath="LoadBalancerStatus", namespace=self.namespace)
state = STATE_MAPPINGS.get(status, State.UNKNOWN)
address = findtext(element=el, xpath="Address", namespace=self.namespace)
extra = self._get_extra_dict(el, RESOURCE_EXTRA_ATTRIBUTES_MAP["balancer"])
balancer = LoadBalancer(
id=_id,
name=name,
state=state,
ip=address,
port=None,
driver=self,
extra=extra,
)
return balancer
def _create_list_params(self, params, items, label):
"""
return parameter list
"""
if isinstance(items, str):
items = [items]
for index, item in enumerate(items):
params[label % (index + 1)] = item
return params
def _get_extra_dict(self, element, mapping):
"""
Extract attributes from the element based on rules provided in the
mapping dictionary.
:param element: Element to parse the values from.
:type element: xml.etree.ElementTree.Element.
:param mapping: Dictionary with the extra layout
:type node: :class:`Node`
:rtype: ``dict``
"""
extra = {}
for attribute, values in mapping.items():
transform_func = values["transform_func"]
value = findattr(element=element, xpath=values["xpath"], namespace=self.namespace)
if value:
try:
extra[attribute] = transform_func(value)
except Exception:
extra[attribute] = None
else:
extra[attribute] = value
return extra
def _to_servers_json(self, members):
servers = []
for each in members:
server = {"ServerId": each.id, "Weight": "100"}
if "Weight" in each.extra:
server["Weight"] = each.extra["Weight"]
servers.append(server)
try:
return json.dumps(servers)
except Exception:
raise AttributeError("could not convert member to backend server")
def _to_balancer_attribute(self, element):
balancer = self._to_balancer(element)
port_proto_elements = findall(
element,
"ListenerPortsAndProtocol/ListenerPortAndProtocol",
namespace=self.namespace,
)
if len(port_proto_elements) > 0:
listeners = [self._to_port_and_protocol(el) for el in port_proto_elements]
else:
port_elements = findall(element, "ListenerPorts/ListenerPort", namespace=self.namespace)
listeners = [
{"ListenerPort": el.text, "ListenerProtocol": "http"} for el in port_elements
]
server_elements = findall(element, "BackendServers/BackendServer", namespace=self.namespace)
backend_servers = [self._to_server_and_weight(el) for el in server_elements]
return SLBLoadBalancerAttribute(balancer, listeners, backend_servers)
def _to_port_and_protocol(self, el):
port = findtext(el, "ListenerPort", namespace=self.namespace)
protocol = findtext(el, "ListenerProtocol", namespace=self.namespace)
return {"ListenerPort": port, "ListenerProtocol": protocol}
def _to_server_and_weight(self, el):
server_id = findtext(el, "ServerId", namespace=self.namespace)
weight = findtext(el, "Weight", namespace=self.namespace)
return {"ServerId": server_id, "Weight": weight}
def _to_server_certificate(self, el):
_id = findtext(el, "ServerCertificateId", namespace=self.namespace)
name = findtext(el, "ServerCertificateName", namespace=self.namespace)
fingerprint = findtext(el, "Fingerprint", namespace=self.namespace)
return SLBServerCertificate(id=_id, name=name, fingerprint=fingerprint)
def _list_to_json(self, value):
try:
return json.dumps(value)
except Exception:
return "[]"