# Copyright (c) 2014 Rackspace # Copyright (c) 2016 Blue Box, an IBM Company # 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 re from sqlalchemy.orm import collections from octavia.common import constants class BaseDataModel(object): # NOTE(brandon-logan) This does not discover dicts for relationship # attributes. def to_dict(self): """Converts a data model to a dictionary.""" ret = {} for attr in self.__dict__: if attr.startswith('_'): continue if isinstance(getattr(self, attr), (BaseDataModel, list)): ret[attr] = None else: ret[attr] = self.__dict__[attr] return ret def __eq__(self, other): if isinstance(other, self.__class__): return self.to_dict() == other.to_dict() return False @classmethod def from_dict(cls, dict): return cls(**dict) @classmethod def _name(cls): """Returns class name in a more human readable form.""" # Split the class name up by capitalized words return ' '.join(re.findall('[A-Z][^A-Z]*', cls.__name__)) def _get_unique_key(self, obj=None): """Returns a unique key for passed object for data model building.""" obj = obj or self # First handle all objects with their own ID, then handle subordinate # objects. if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer', 'Listener', 'Amphora', 'L7Policy', 'L7Rule']: return obj.__class__.__name__ + obj.id elif obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']: return obj.__class__.__name__ + obj.pool_id elif obj.__class__.__name__ in ['ListenerStatistics']: return obj.__class__.__name__ + obj.listener_id elif obj.__class__.__name__ in ['VRRPGroup', 'Vip']: return obj.__class__.__name__ + obj.load_balancer_id elif obj.__class__.__name__ in ['AmphoraHealth']: return obj.__class__.__name__ + obj.amphora_id elif obj.__class__.__name__ in ['SNI']: return (obj.__class__.__name__ + obj.listener_id + obj.tls_container_id) else: raise NotImplementedError def _find_in_graph(self, key, _visited_nodes=None): """Locates an object with the given unique key in the current object graph and returns a reference to it. """ _visited_nodes = _visited_nodes or [] mykey = self._get_unique_key() if mykey in _visited_nodes: # Seen this node already, don't traverse further return None elif mykey == key: return self else: _visited_nodes.append(mykey) attr_names = [attr_name for attr_name in dir(self) if not attr_name.startswith('_')] for attr_name in attr_names: attr = getattr(self, attr_name) if isinstance(attr, BaseDataModel): result = attr._find_in_graph( key, _visited_nodes=_visited_nodes) if result is not None: return result elif isinstance(attr, (collections.InstrumentedList, list)): for item in attr: if isinstance(item, BaseDataModel): result = item._find_in_graph( key, _visited_nodes=_visited_nodes) if result is not None: return result # If we are here we didn't find it. return None def update(self, update_dict): """Generic update method which works for simple, non-relational attributes. """ for key, value in update_dict.items(): setattr(self, key, value) class SessionPersistence(BaseDataModel): def __init__(self, pool_id=None, type=None, cookie_name=None, pool=None): self.pool_id = pool_id self.type = type self.cookie_name = cookie_name self.pool = pool def delete(self): self.pool.session_persistence = None class ListenerStatistics(BaseDataModel): def __init__(self, listener_id=None, amphora_id=None, bytes_in=None, bytes_out=None, active_connections=None, total_connections=None, listener=None): self.listener_id = listener_id self.amphora_id = amphora_id self.bytes_in = bytes_in self.bytes_out = bytes_out self.active_connections = active_connections self.total_connections = total_connections self.listener = listener def delete(self): self.listener.stats = None class HealthMonitor(BaseDataModel): def __init__(self, id=None, project_id=None, pool_id=None, type=None, delay=None, timeout=None, fall_threshold=None, rise_threshold=None, http_method=None, url_path=None, expected_codes=None, enabled=None, pool=None): self.id = id self.project_id = project_id self.pool_id = pool_id self.type = type self.delay = delay self.timeout = timeout self.fall_threshold = fall_threshold self.rise_threshold = rise_threshold self.http_method = http_method self.url_path = url_path self.expected_codes = expected_codes self.enabled = enabled self.pool = pool def delete(self): self.pool.health_monitor = None class Pool(BaseDataModel): def __init__(self, id=None, project_id=None, name=None, description=None, protocol=None, lb_algorithm=None, enabled=None, operating_status=None, members=None, health_monitor=None, session_persistence=None, load_balancer_id=None, load_balancer=None, listeners=None, l7policies=None, created_at=None, updated_at=None): self.id = id self.project_id = project_id self.name = name self.description = description self.load_balancer_id = load_balancer_id self.load_balancer = load_balancer self.protocol = protocol self.lb_algorithm = lb_algorithm self.enabled = enabled self.operating_status = operating_status self.members = members or [] self.health_monitor = health_monitor self.session_persistence = session_persistence self.listeners = listeners or [] self.l7policies = l7policies or [] self.created_at = created_at self.updated_at = updated_at def update(self, update_dict): for key, value in update_dict.items(): if key == 'session_persistence': if value is None or value == {}: if self.session_persistence is not None: self.session_persistence.delete() elif self.session_persistence is not None: self.session_persistence.update(value) else: value.update({'pool_id': self.id}) self.session_persistence = SessionPersistence(**value) else: setattr(self, key, value) def delete(self): for listener in self.listeners: if listener.default_pool_id == self.id: listener.default_pool = None listener.default_pool_id = None for pool in listener.pools: if pool.id == self.id: listener.pools.remove(pool) break for pool in self.load_balancer.pools: if pool.id == self.id: self.load_balancer.pools.remove(pool) break for l7policy in self.l7policies: if l7policy.redirect_pool_id == self.id: # Technically this should never happen, as we block deletion # of pools in use by L7Policies at the API. However, we should # probably keep this here in case the data model gets # manipulated in some other way in the future. l7policy.action = constants.L7POLICY_ACTION_REJECT l7policy.redirect_pool = None l7policy.redirect_pool_id = None class Member(BaseDataModel): def __init__(self, id=None, project_id=None, pool_id=None, ip_address=None, protocol_port=None, weight=None, enabled=None, subnet_id=None, operating_status=None, pool=None, created_at=None, updated_at=None): self.id = id self.project_id = project_id self.pool_id = pool_id self.ip_address = ip_address self.protocol_port = protocol_port self.weight = weight self.enabled = enabled self.subnet_id = subnet_id self.operating_status = operating_status self.pool = pool self.created_at = created_at self.updated_at = updated_at def delete(self): for mem in self.pool.members: if mem.id == self.id: self.pool.members.remove(mem) break class Listener(BaseDataModel): def __init__(self, id=None, project_id=None, name=None, description=None, default_pool_id=None, load_balancer_id=None, protocol=None, protocol_port=None, connection_limit=None, enabled=None, provisioning_status=None, operating_status=None, tls_certificate_id=None, stats=None, default_pool=None, load_balancer=None, sni_containers=None, peer_port=None, l7policies=None, pools=None, insert_headers=None, created_at=None, updated_at=None): self.id = id self.project_id = project_id self.name = name self.description = description self.default_pool_id = default_pool_id self.load_balancer_id = load_balancer_id self.protocol = protocol self.protocol_port = protocol_port self.connection_limit = connection_limit self.enabled = enabled self.provisioning_status = provisioning_status self.operating_status = operating_status self.tls_certificate_id = tls_certificate_id self.stats = stats self.default_pool = default_pool self.load_balancer = load_balancer self.sni_containers = sni_containers or [] self.peer_port = peer_port self.l7policies = l7policies or [] self.insert_headers = insert_headers or {} self.pools = pools or [] self.created_at = created_at self.updated_at = updated_at def update(self, update_dict): for key, value in update_dict.items(): setattr(self, key, value) if key == 'default_pool_id': if self.default_pool is not None: l7_pool_ids = [p.redirect_pool_id for p in self.l7policies if p.redirect_pool_id is not None and len(p.l7rules) > 0 and p.enabled is True] old_pool = self.default_pool if old_pool.id not in l7_pool_ids: self.pools.remove(old_pool) old_pool.listeners.remove(self) if value is not None: pool = self._find_in_graph('Pool' + value) if pool not in self.pools: self.pools.append(pool) if self not in pool.listeners: pool.listeners.append(self) else: pool = None setattr(self, 'default_pool', pool) def delete(self): for listener in self.load_balancer.listeners: if listener.id == self.id: self.load_balancer.listeners.remove(listener) break for pool in self.pools: pool.listeners.remove(self) class LoadBalancer(BaseDataModel): def __init__(self, id=None, project_id=None, name=None, description=None, provisioning_status=None, operating_status=None, enabled=None, topology=None, vip=None, listeners=None, amphorae=None, pools=None, vrrp_group=None, server_group_id=None, created_at=None, updated_at=None): self.id = id self.project_id = project_id self.name = name self.description = description self.provisioning_status = provisioning_status self.operating_status = operating_status self.enabled = enabled self.vip = vip self.vrrp_group = vrrp_group self.topology = topology self.listeners = listeners or [] self.amphorae = amphorae or [] self.pools = pools or [] self.server_group_id = server_group_id self.created_at = created_at self.updated_at = updated_at class VRRPGroup(BaseDataModel): def __init__(self, load_balancer_id=None, vrrp_group_name=None, vrrp_auth_type=None, vrrp_auth_pass=None, advert_int=None, smtp_server=None, smtp_connect_timeout=None, load_balancer=None): self.load_balancer_id = load_balancer_id self.vrrp_group_name = vrrp_group_name self.vrrp_auth_type = vrrp_auth_type self.vrrp_auth_pass = vrrp_auth_pass self.advert_int = advert_int self.load_balancer = load_balancer class Vip(BaseDataModel): def __init__(self, load_balancer_id=None, ip_address=None, subnet_id=None, port_id=None, load_balancer=None): self.load_balancer_id = load_balancer_id self.ip_address = ip_address self.subnet_id = subnet_id self.port_id = port_id self.load_balancer = load_balancer class SNI(BaseDataModel): def __init__(self, listener_id=None, position=None, listener=None, tls_container_id=None): self.listener_id = listener_id self.position = position self.listener = listener self.tls_container_id = tls_container_id class TLSContainer(BaseDataModel): def __init__(self, id=None, primary_cn=None, certificate=None, private_key=None, passphrase=None, intermediates=None): self.id = id self.primary_cn = primary_cn self.certificate = certificate self.private_key = private_key self.passphrase = passphrase self.intermediates = intermediates or [] class Amphora(BaseDataModel): def __init__(self, id=None, load_balancer_id=None, compute_id=None, status=None, lb_network_ip=None, vrrp_ip=None, ha_ip=None, vrrp_port_id=None, ha_port_id=None, load_balancer=None, role=None, cert_expiration=None, cert_busy=False, vrrp_interface=None, vrrp_id=None, vrrp_priority=None): self.id = id self.load_balancer_id = load_balancer_id self.compute_id = compute_id self.status = status self.lb_network_ip = lb_network_ip self.vrrp_ip = vrrp_ip self.ha_ip = ha_ip self.vrrp_port_id = vrrp_port_id self.ha_port_id = ha_port_id self.role = role self.vrrp_interface = vrrp_interface self.vrrp_id = vrrp_id self.vrrp_priority = vrrp_priority self.load_balancer = load_balancer self.cert_expiration = cert_expiration self.cert_busy = cert_busy def delete(self): for amphora in self.load_balancer.amphorae: if amphora.id == self.id: self.load_balancer.amphorae.remove(amphora) break class AmphoraHealth(BaseDataModel): def __init__(self, amphora_id=None, last_update=None, busy=False): self.amphora_id = amphora_id self.last_update = last_update self.busy = busy class L7Rule(BaseDataModel): def __init__(self, id=None, l7policy_id=None, type=None, compare_type=None, key=None, value=None, l7policy=None, invert=False): self.id = id self.l7policy_id = l7policy_id self.type = type self.compare_type = compare_type self.key = key self.value = value self.l7policy = l7policy self.invert = invert def delete(self): if len(self.l7policy.l7rules) == 1: # l7policy should disappear from pool and listener lists. Since # we are operating only on the data model, we can fake this by # calling the policy's delete method. self.l7policy.delete() for r in self.l7policy.l7rules: if r.id == self.id: self.l7policy.l7rules.remove(r) break class L7Policy(BaseDataModel): def __init__(self, id=None, name=None, description=None, listener_id=None, action=None, redirect_pool_id=None, redirect_url=None, position=None, listener=None, redirect_pool=None, enabled=None, l7rules=None): self.id = id self.name = name self.description = description self.listener_id = listener_id self.action = action self.redirect_pool_id = redirect_pool_id self.redirect_url = redirect_url self.position = position self.listener = listener self.redirect_pool = redirect_pool self.enabled = enabled self.l7rules = l7rules or [] def _conditionally_remove_pool_links(self, pool): """Removes links to the given pool from parent objects. Note this only happens if our listener isn't referencing the pool via its default_pool or another active l7policy's redirect_pool_id. """ if (self.listener.default_pool is not None and pool is not None and pool.id != self.listener.default_pool.id and pool in self.listener.pools): listener_l7pools = [ p.redirect_pool for p in self.listener.l7policies if p.redirect_pool is not None and len(p.l7rules) > 0 and p.enabled is True and p.id != self.id] if pool not in listener_l7pools: self.listener.pools.remove(pool) pool.listeners.remove(self.listener) def update(self, update_dict): for key, value in update_dict.items(): if key == 'redirect_pool_id' and value is not None: self._conditionally_remove_pool_links(self.redirect_pool) self.action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL self.redirect_url = None pool = self._find_in_graph('Pool' + value) self.redirect_pool = pool if len(self.l7rules) > 0 and (self.enabled is True or ('enabled' in update_dict.keys() and update_dict['enabled'] is True)): if pool not in self.listener.pools: self.listener.pools.append(pool) if self.listener not in pool.listeners: pool.listeners.append(self.listener) elif key == 'redirect_url' and value is not None: self.action = constants.L7POLICY_ACTION_REDIRECT_TO_URL self._conditionally_remove_pool_links(self.redirect_pool) self.redirect_pool = None self.redirect_pool_id = None elif key == 'action' and value == constants.L7POLICY_ACTION_REJECT: self.redirect_url = None self._conditionally_remove_pool_links(self.redirect_pool) self.redirect_pool = None self.redirect_pool_id = None elif key == 'position': self.listener.l7policies.remove(self) self.listener.l7policies.insert(value - 1, self) elif key == 'enabled': if (value is True and self.action == constants.L7POLICY_ACTION_REDIRECT_TO_POOL and self.redirect_pool is not None and len(self.l7rules) > 0 and self.redirect_pool not in self.listener.pools): self.listener.pools.append(self.redirect_pool) self.redirect_pool.listeners.append(self.listener) elif (value is False and self.action == constants.L7POLICY_ACTION_REDIRECT_TO_POOL and self.redirect_pool is not None): self._conditionally_remove_pool_links( self.redirect_pool) setattr(self, key, value) def delete(self): self._conditionally_remove_pool_links(self.redirect_pool) if self.redirect_pool: for p in self.redirect_pool.l7policies: if p.id == self.id: self.redirect_pool.l7policies.remove(p) for p in self.listener.l7policies: if p.id == self.id: self.listener.l7policies.remove(p) break