Files
quark/quark/db/models.py
Amir Sadoughi c6ca88568d Re-implemented IP policies with offset and length
* Implemented CRUD ip_policies plugin methods
* Implemented plugin ip_policy unit tests
* Added IP Policy logic to ipam.allocate_ip_address
* Implemented ip_policy ipam unit tests
* Implemented ip_policies extension
* Added ip_policies db models
* Implemented db_api ip_policy methods
* Implemented _make_ip_policy_dict view
* Fixed existing tests

An IP Policy is defined on a subnet or network definining what IPs to
exclude from allocation. From a user perspective an IP Policy looks
like the following:

{
  "id": <id>,
  "tenant_id": <tenant_id>,
  "name": "foobar",
  "subnet_ids": [] (list of uuids),
  "network_ids": [] (list of uuids),
  "exclude": list of dictionaries (e.g. [{"offset": -1, "length": 3}])
}

"exclude" is always required. One of "subnet_ids" or "network_ids"
is required. Only one policy is allowed per network.

A default policy is enabled on all subnets if no ip policies are
specified on that subnet or its network. This default policy is
established via JSON configuration as "default_ip_policy". The
default for "default_ip_policy" is no policy.
2013-08-06 02:24:32 -05:00

417 lines
15 KiB
Python

# Copyright 2013 Openstack Foundation
# 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 netaddr
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext import associationproxy
from sqlalchemy.ext import declarative
from sqlalchemy.ext import hybrid
import neutron.db.model_base
from neutron.db import models_v2 as models
from neutron.openstack.common import log as logging
from neutron.openstack.common import timeutils
from oslo.config import cfg
from quark.db import custom_types
import json
HasId = models.HasId
LOG = logging.getLogger("neutron.quark.db.models")
CONF = cfg.CONF
quark_opts = [
cfg.StrOpt('default_ip_policy', default='{}',
help=_("Default IP allocation policy"))
]
CONF.register_opts(quark_opts, "QUARK")
def _default_list_getset(collection_class, proxy):
attr = proxy.value_attr
def getter(obj):
if obj:
return getattr(obj, attr, None)
return []
if collection_class is dict:
setter = lambda o, k, v: setattr(o, attr, v)
else:
setter = lambda o, v: setattr(o, attr, v)
return getter, setter
class QuarkBase(neutron.db.model_base.NeutronBaseV2):
created_at = sa.Column(sa.DateTime(), default=timeutils.utcnow)
__table_args__ = {"mysql_engine": "InnoDB"}
BASEV2 = declarative.declarative_base(cls=QuarkBase)
class TagAssociation(BASEV2, models.HasId):
__tablename__ = "quark_tag_associations"
discriminator = sa.Column(sa.String(255))
tags = associationproxy.association_proxy("tags_association", "tag",
creator=lambda t: Tag(tag=t))
@classmethod
def creator(cls, discriminator):
return lambda tags: TagAssociation(tags=tags,
discriminator=discriminator)
@property
def parent(self):
"""Return the parent object."""
return getattr(self, "%s_parent" % self.discriminator)
class Tag(BASEV2, models.HasId, models.HasTenant):
__tablename__ = "quark_tags"
association_uuid = sa.Column(sa.String(36),
sa.ForeignKey(TagAssociation.id),
nullable=False)
tag = sa.Column(sa.String(255), nullable=False)
parent = associationproxy.association_proxy("association", "parent")
association = orm.relationship("TagAssociation",
backref=orm.backref("tags_association"))
class IsHazTags(object):
@declarative.declared_attr
def tag_association_uuid(cls):
return sa.Column(sa.String(36), sa.ForeignKey(TagAssociation.id),
nullable=True)
@declarative.declared_attr
def tag_association(cls):
discriminator = cls.__name__.lower()
creator = TagAssociation.creator(discriminator)
kwargs = {'creator': creator,
'getset_factory': _default_list_getset}
cls.tags = associationproxy.association_proxy("tag_association",
"tags", **kwargs)
backref = orm.backref("%s_parent" % discriminator, uselist=False)
return orm.relationship("TagAssociation", backref=backref)
class IPAddress(BASEV2, models.HasId, models.HasTenant):
"""More closely emulate the melange version of the IP table.
We always mark the record as deallocated rather than deleting it.
Gives us an IP address owner audit log for free, essentially.
"""
__tablename__ = "quark_ip_addresses"
address_readable = sa.Column(sa.String(128), nullable=False)
address = sa.Column(custom_types.INET(), nullable=False)
subnet_id = sa.Column(sa.String(36),
sa.ForeignKey("quark_subnets.id",
ondelete="CASCADE"))
network_id = sa.Column(sa.String(36),
sa.ForeignKey("quark_networks.id",
ondelete="CASCADE"))
version = sa.Column(sa.Integer())
# Need a constant to facilitate the indexed search for new IPs
_deallocated = sa.Column(sa.Boolean())
@hybrid.hybrid_property
def deallocated(self):
return self._deallocated and not self.ports
@deallocated.setter
def deallocated(self, val):
self._deallocated = val
self.deallocated_at = None
if val:
self.deallocated_at = timeutils.utcnow()
# TODO(jkoelker) update the expression to use the jointable as well
@deallocated.expression
def deallocated(cls):
return IPAddress._deallocated
def formatted(self):
ip = netaddr.IPAddress(self.address_readable)
if self.version == 4:
return str(ip.ipv4())
return str(ip.ipv6())
deallocated_at = sa.Column(sa.DateTime())
class Route(BASEV2, models.HasTenant, models.HasId, IsHazTags):
__tablename__ = "quark_routes"
cidr = sa.Column(sa.String(64))
gateway = sa.Column(sa.String(64))
subnet_id = sa.Column(sa.String(36), sa.ForeignKey("quark_subnets.id",
ondelete="CASCADE"))
class DNSNameserver(BASEV2, models.HasTenant, models.HasId, IsHazTags):
__tablename__ = "quark_dns_nameservers"
ip = sa.Column(custom_types.INET())
subnet_id = sa.Column(sa.String(36), sa.ForeignKey("quark_subnets.id",
ondelete="CASCADE"))
class Subnet(BASEV2, models.HasId, models.HasTenant, IsHazTags):
"""Upstream model for IPs.
Subnet -> has_many(IPAllocationPool)
IPAllocationPool -> has_many(IPAvailabilityRange)
As well as first and last _ip markers for some unknown reason
first_ip is min(ranges), last_ip is max(ranges)
IPAvailabilityRange -> belongs_to(IPAllocationPool)
Also has first and last _ip, but only for the range
IPAllocation -> belongs_to(port, subnet, network) but NOT IPAllocationPool
IPAllocationPool and Range seem superfluous. Just create intelligent CIDRs
for your subnet
"""
__tablename__ = "quark_subnets"
name = sa.Column(sa.String(255))
network_id = sa.Column(sa.String(36), sa.ForeignKey('quark_networks.id'))
_cidr = sa.Column(sa.String(64), nullable=False)
@hybrid.hybrid_property
def cidr(self):
return self._cidr
@cidr.setter
def cidr(self, val):
self._cidr = val
preip = netaddr.IPNetwork(val)
self.ip_version = preip.version
ip = netaddr.IPNetwork(val).ipv6()
self.first_ip = ip.first
self.last_ip = ip.last
self.next_auto_assign_ip = self.first_ip
@cidr.expression
def cidr(cls):
return Subnet._cidr
first_ip = sa.Column(custom_types.INET())
last_ip = sa.Column(custom_types.INET())
ip_version = sa.Column(sa.Integer())
next_auto_assign_ip = sa.Column(custom_types.INET())
allocated_ips = orm.relationship(IPAddress,
primaryjoin='and_(Subnet.id=='
'IPAddress.subnet_id, '
'IPAddress._deallocated!=1)',
backref="subnet")
routes = orm.relationship(Route, primaryjoin="Route.subnet_id==Subnet.id",
backref='subnet', cascade='delete')
enable_dhcp = sa.Column(sa.Boolean(), default=False)
dns_nameservers = orm.relationship(
DNSNameserver,
primaryjoin="DNSNameserver.subnet_id==Subnet.id",
backref='subnet',
cascade='delete')
ip_policy_id = sa.Column(sa.String(36),
sa.ForeignKey("quark_ip_policy.id"))
port_ip_association_table = sa.Table(
"quark_port_ip_address_associations",
BASEV2.metadata,
sa.Column("port_id", sa.String(36),
sa.ForeignKey("quark_ports.id")),
sa.Column("ip_address_id", sa.String(36),
sa.ForeignKey("quark_ip_addresses.id")))
port_group_association_table = sa.Table(
"quark_port_security_group_associations",
BASEV2.metadata,
sa.Column("port_id", sa.String(36),
sa.ForeignKey("quark_ports.id")),
sa.Column("group_id", sa.String(36),
sa.ForeignKey("quark_security_groups.id")))
class SecurityGroupRule(BASEV2, models.HasId, models.HasTenant):
__tablename__ = "quark_security_group_rule"
id = sa.Column(sa.String(36), primary_key=True)
group_id = sa.Column(sa.String(36),
sa.ForeignKey("quark_security_groups.id"),
nullable=False)
direction = sa.Column(sa.String(10), nullable=False)
ethertype = sa.Column(sa.String(4), nullable=False)
port_range_max = sa.Column(sa.Integer(), nullable=True)
port_range_min = sa.Column(sa.Integer(), nullable=True)
protocol = sa.Column(sa.Integer(), nullable=True)
remote_ip_prefix = sa.Column(sa.String(22), nullable=True)
remote_group_id = sa.Column(sa.String(36), nullable=True)
class SecurityGroup(BASEV2, models.HasId, models.HasTenant):
__tablename__ = "quark_security_groups"
id = sa.Column(sa.String(36), primary_key=True)
name = sa.Column(sa.String(255), nullable=False)
description = sa.Column(sa.String(255), nullable=False)
join = "SecurityGroupRule.group_id==SecurityGroup.id"
rules = orm.relationship(SecurityGroupRule, backref='group',
cascade='delete',
primaryjoin=join)
class Port(BASEV2, models.HasTenant, models.HasId):
__tablename__ = "quark_ports"
id = sa.Column(sa.String(36), primary_key=True)
name = sa.Column(sa.String(255))
admin_state_up = sa.Column(sa.Boolean(), default=True)
network_id = sa.Column(sa.String(36), sa.ForeignKey("quark_networks.id"),
nullable=False)
backend_key = sa.Column(sa.String(36), nullable=False)
mac_address = sa.Column(sa.BigInteger())
device_id = sa.Column(sa.String(255), nullable=False)
device_owner = sa.Column(sa.String(255))
@declarative.declared_attr
def ip_addresses(cls):
primaryjoin = cls.id == port_ip_association_table.c.port_id
secondaryjoin = (port_ip_association_table.c.ip_address_id ==
IPAddress.id)
return orm.relationship(IPAddress, primaryjoin=primaryjoin,
secondaryjoin=secondaryjoin,
secondary=port_ip_association_table,
backref="ports")
@declarative.declared_attr
def security_groups(cls):
primaryjoin = cls.id == port_group_association_table.c.port_id
secondaryjoin = (port_group_association_table.c.group_id ==
SecurityGroup.id)
return orm.relationship(SecurityGroup, primaryjoin=primaryjoin,
secondaryjoin=secondaryjoin,
secondary=port_group_association_table,
backref="ports")
class MacAddress(BASEV2, models.HasTenant):
__tablename__ = "quark_mac_addresses"
address = sa.Column(sa.BigInteger(), primary_key=True)
mac_address_range_id = sa.Column(
sa.String(36),
sa.ForeignKey("quark_mac_address_ranges.id", ondelete="CASCADE"),
nullable=False)
deallocated = sa.Column(sa.Boolean())
deallocated_at = sa.Column(sa.DateTime())
orm.relationship(Port, backref="mac_address")
class MacAddressRange(BASEV2, models.HasId):
__tablename__ = "quark_mac_address_ranges"
cidr = sa.Column(sa.String(255), nullable=False)
first_address = sa.Column(sa.BigInteger(), nullable=False)
last_address = sa.Column(sa.BigInteger(), nullable=False)
next_auto_assign_mac = sa.Column(sa.BigInteger(), nullable=False)
allocated_macs = orm.relationship(MacAddress,
primaryjoin='and_(MacAddressRange.id=='
'MacAddress.mac_address_range_id, '
'MacAddress.deallocated!=1)',
backref="mac_address_range")
class IPPolicy(BASEV2, models.HasId, models.HasTenant):
__tablename__ = "quark_ip_policy"
networks = orm.relationship(
"Network",
primaryjoin="IPPolicy.id==Network.ip_policy_id",
backref="ip_policy")
subnets = orm.relationship(
"Subnet",
primaryjoin="IPPolicy.id==Subnet.ip_policy_id",
backref="ip_policy")
exclude = orm.relationship(
"IPPolicyRange",
primaryjoin="IPPolicy.id==IPPolicyRange.ip_policy_id",
backref="ip_policy")
name = sa.Column(sa.String(255), nullable=True)
class JSONIPPolicy(object):
def __init__(self, policy=None):
self.policy = {}
if not policy:
self._compile_policy(CONF.QUARK.default_ip_policy)
else:
self._compile_policy(policy)
def _compile_policy(self, policy):
self.policy = json.loads(policy)
def __getattr__(self, name):
return getattr(self.policy, name)
DEFAULT_POLICY = JSONIPPolicy()
@staticmethod
def get_ip_policy_rule_set(subnet):
ip_policy = subnet["ip_policy"] or \
subnet["network"]["ip_policy"] or \
dict()
ip_policy_ranges = ip_policy.get("exclude", []) + \
IPPolicy.DEFAULT_POLICY.get("exclude", [])
ip_policy_rules = netaddr.IPSet()
subnet_net = netaddr.IPNetwork(subnet["cidr"])
for arange in ip_policy_ranges:
end_index = arange["offset"] + arange["length"]
end_index_wraps_around = (arange["offset"] < 0 and end_index >= 0)
if end_index_wraps_around:
second_index = min(end_index, len(subnet_net))
end_index = None
ip_policy_rules |= netaddr.IPSet(subnet_net[:second_index])
ip_policy_rules |= netaddr.IPSet(
subnet_net[arange["offset"]:end_index])
return ip_policy_rules
class IPPolicyRange(BASEV2, models.HasId):
__tablename__ = "quark_ip_policy_rules"
ip_policy_id = sa.Column(sa.String(36), sa.ForeignKey(
"quark_ip_policy.id", ondelete="CASCADE"))
offset = sa.Column(sa.Integer())
length = sa.Column(sa.Integer())
class Network(BASEV2, models.HasTenant, models.HasId):
__tablename__ = "quark_networks"
name = sa.Column(sa.String(255))
ports = orm.relationship(Port, backref='network')
subnets = orm.relationship(Subnet, backref='network')
ip_policy_id = sa.Column(sa.String(36),
sa.ForeignKey("quark_ip_policy.id"))