Fixes to PAT and DNS support.
This commit is contained in:
parent
c1239d9a5c
commit
d83fca3537
@ -15,16 +15,14 @@ import json
|
||||
import logging
|
||||
import uuid
|
||||
from Crypto.PublicKey import RSA
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from tatu.config import CONF
|
||||
from tatu.db import models as db
|
||||
from tatu.dns import add_srv_records
|
||||
from tatu.pat import create_pat_entries
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
from tatu.db import models as db
|
||||
|
||||
|
||||
def validate_uuid(map, key):
|
||||
@ -237,7 +235,7 @@ class NovaVendorData(object):
|
||||
# TODO(pino): make the whole workflow fault-tolerant
|
||||
# TODO(pino): make this configurable per project or subnet
|
||||
if CONF.tatu.use_pat_bastion:
|
||||
pat_entries = create_pat_entries(req.body['instance-id'], 22,
|
||||
num=CONF.tatu.bastion_redundancy)
|
||||
add_srv_records(req.body['project-id'], req.body['hostname'],
|
||||
pat_entries)
|
||||
port_ip_tuples = create_pat_entries(self.session,
|
||||
req.body['instance-id'], 22)
|
||||
add_srv_records(req.body['hostname'], req.body['project-id'],
|
||||
port_ip_tuples)
|
||||
|
@ -10,22 +10,52 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from designateclient.v2 import client as designate_client
|
||||
from dragonflow import conf as dragonflow_cfg
|
||||
from dragonflow.db import api_nb
|
||||
from keystoneauth1 import session as keystone_session
|
||||
from keystoneauth1.identity import v3
|
||||
from novaclient import client as nova_client
|
||||
from neutronclient.v2_0 import client as neutron_client
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from castellan.options import set_defaults as set_castellan_defaults
|
||||
from tatu import castellano
|
||||
import sys
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
# 3 steps: register options; read the config file; use the options
|
||||
|
||||
# 1) register options; 2) read the config file; 3) use the options
|
||||
opts = [
|
||||
cfg.BoolOpt('use_barbican_key_manager', default=False,
|
||||
help='Enable the usage of the OpenStack Key Management '
|
||||
'service provided by barbican.'),
|
||||
help='Use OpenStack Barbican to store sensitive data'),
|
||||
cfg.BoolOpt('use_pat_bastions', default=True,
|
||||
help='Use PAT as a "poor man\'s" approach to bastions'),
|
||||
cfg.IntOpt('num_total_pats', default=3,
|
||||
help='Number of available PAT addresses for bastions'),
|
||||
cfg.IntOpt('num_pat_bastions_per_server', default=2,
|
||||
help='Number of PAT bastions per server for redundancy'),
|
||||
cfg.StrOpt('pat_dns_zone_name',
|
||||
default='tatuPAT.com.',
|
||||
help='Name of DNS zone for A and SRV records for PAT bastions'),
|
||||
cfg.StrOpt('pat_dns_zone_email',
|
||||
default='tatu@nono.nono',
|
||||
help='Email of admin for DNS zone for PAT bastions'),
|
||||
cfg.StrOpt('sqlalchemy_engine',
|
||||
default='mysql+pymysql://root:pinot@127.0.0.1/neutron?charset=utf8',
|
||||
help='SQLAlchemy database URL'),
|
||||
cfg.StrOpt('auth_url',
|
||||
default='http://localhost/identity/v3',
|
||||
help='OpenStack Keystone URL'),
|
||||
cfg.StrOpt('user_id',
|
||||
default='fab01a1f2a7749b78a53dffe441a1879',
|
||||
help='OpenStack Keystone admin privileged user-id'),
|
||||
cfg.StrOpt('password',
|
||||
default='pinot',
|
||||
help='OpenStack Keystone password'),
|
||||
cfg.StrOpt('project_id',
|
||||
default='2e6c998ad16f4045821304470a57d160',
|
||||
help='OpenStack Keystone admin project UUID'),
|
||||
]
|
||||
|
||||
DOMAIN = "tatu"
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(opts, group='tatu')
|
||||
|
||||
@ -33,14 +63,18 @@ logging.register_options(CONF)
|
||||
log_levels = logging.get_default_log_levels() + \
|
||||
['tatu=DEBUG', '__main__=DEBUG']
|
||||
logging.set_defaults(default_log_levels=log_levels)
|
||||
#CONF(default_config_files=cfg.find_config_files(project='tatu', prog='tatu'))
|
||||
|
||||
try:
|
||||
CONF(args=[], default_config_files=['files/tatu.conf'])
|
||||
CONF(args=[],
|
||||
default_config_files=['/etc/tatu/tatu.conf',
|
||||
'tatu.conf',
|
||||
'files/tatu.conf'
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error("Failed to load configuration file: {}".format(e))
|
||||
|
||||
logging.setup(CONF, DOMAIN)
|
||||
logging.setup(CONF, "tatu")
|
||||
if CONF.tatu.use_barbican_key_manager:
|
||||
LOG.debug("Using Barbican as key manager.")
|
||||
set_castellan_defaults(CONF)
|
||||
@ -49,3 +83,15 @@ else:
|
||||
set_castellan_defaults(CONF,
|
||||
api_class='tatu.castellano.TatuKeyManager')
|
||||
|
||||
auth = v3.Password(auth_url=CONF.tatu.auth_url,
|
||||
user_id=CONF.tatu.user_id,
|
||||
password=CONF.tatu.password,
|
||||
project_id=CONF.tatu.project_id)
|
||||
session = keystone_session.Session(auth=auth)
|
||||
NOVA = nova_client.Client('2', session=session)
|
||||
NEUTRON = neutron_client.Client(session=session)
|
||||
DESIGNATE = designate_client.Client(session=session)
|
||||
|
||||
dragonflow_cfg.CONF(default_config_files=['/etc/neutron/dragonflow.ini'])
|
||||
dragonflow_cfg.CONF.set_override('enable_df_pub_sub', False, group='df')
|
||||
DRAGONFLOW = api_nb.NbApi.get_instance(False)
|
||||
|
@ -200,3 +200,17 @@ def createHostCert(session, token_id, host_id, pub):
|
||||
session.add(token)
|
||||
session.commit()
|
||||
return host
|
||||
|
||||
|
||||
class L4PortReservation(Base):
|
||||
__tablename__ = 'port_reservation'
|
||||
ip_address = sa.Column(sa.String(36), primary_key=True)
|
||||
# For now, just auto-increment the l4 port. Later, we'll reuse them.
|
||||
l4_port = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
|
||||
def reserve_l4_port(session, ip):
|
||||
rsv = L4PortReservation(ip_address=str(ip))
|
||||
session.add(rsv)
|
||||
session.commit()
|
||||
return rsv.l4_port
|
||||
|
@ -14,18 +14,13 @@ import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
from tatu import config
|
||||
from tatu.db.models import Base
|
||||
|
||||
|
||||
def get_url():
|
||||
return os.getenv("DATABASE_URL", "sqlite:///development.db")
|
||||
# return os.getenv("DATABASE_URL", "sqlite:///:memory:")
|
||||
|
||||
|
||||
class SQLAlchemySessionManager(object):
|
||||
|
||||
def __init__(self):
|
||||
self.engine = create_engine(get_url())
|
||||
self.engine = create_engine(config.CONF.tatu.sqlalchemy_engine)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = scoped_session(sessionmaker(self.engine))
|
||||
|
||||
|
70
tatu/dns.py
70
tatu/dns.py
@ -12,56 +12,54 @@
|
||||
|
||||
import os
|
||||
from designateclient.exceptions import Conflict
|
||||
from designateclient.v2 import client
|
||||
from keystoneclient import session
|
||||
from keystoneclient.auth.identity.generic.password import Password
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from tatu.config import CONF, DESIGNATE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
auth = Password(auth_url=os.getenv('OS_AUTH_URL'),
|
||||
username=os.getenv('OS_USERNAME'),
|
||||
password=os.getenv('OS_PASSWORD'),
|
||||
project_name=os.getenv('OS_PROJECT_NAME'),
|
||||
project_domain_id='default',
|
||||
user_domain_id='default')
|
||||
|
||||
s = session.Session(auth=auth)
|
||||
|
||||
client = client.Client(session=s)
|
||||
zone = None
|
||||
bastions = {}
|
||||
ZONE = None
|
||||
|
||||
|
||||
def setup(bastions=[]):
|
||||
# TODO: retrieve the zone name and email from configuration
|
||||
def _setup_zone():
|
||||
try:
|
||||
global zone
|
||||
zone = client.zones.create('julia.com.', email='pino@yahoo.com')
|
||||
global ZONE
|
||||
ZONE = DESIGNATE.zones.create(CONF.tatu.pat_dns_zone_name,
|
||||
email=CONF.tatu.pat_dns_zone_email)
|
||||
except Conflict:
|
||||
pass
|
||||
|
||||
# TODO: fetch all existing bastions
|
||||
|
||||
def bastion_name_from_ip(ip_address):
|
||||
return "bastion-{}.{}".format(ip_address.replace('.', '-'),
|
||||
ZONE['name'])
|
||||
|
||||
|
||||
def add_bastion(ip_address, project_id, project_name, num):
|
||||
bastion_name = "{}-{}-{}.{}".format(str(project_id)[:8], project_name, num,
|
||||
zone['name'])
|
||||
client.recordsets.create(zone['id'], bastion_name, 'A', [ip_address])
|
||||
bastions.add(ip_address, bastion_name)
|
||||
return bastion_name
|
||||
def register_bastion(ip_address):
|
||||
try:
|
||||
DESIGNATE.recordsets.create(ZONE['id'],
|
||||
bastion_name_from_ip(ip_address),
|
||||
'A', [ip_address])
|
||||
except Conflict:
|
||||
pass
|
||||
|
||||
|
||||
def add_srv_records(project_id, hostname, pat_entries):
|
||||
def sync_bastions(ip_addresses):
|
||||
for ip in ip_addresses:
|
||||
register_bastion(ip)
|
||||
|
||||
|
||||
def add_srv_records(hostname, project_id, port_ip_tuples):
|
||||
records = []
|
||||
for pat_entry in pat_entries:
|
||||
b = bastions[pat_entries.pat.ip_address]
|
||||
for port, ip in port_ip_tuples:
|
||||
bastion = bastion_name_from_ip(ip)
|
||||
# SRV record format is: priority weight port A-name
|
||||
records.add(
|
||||
'10 50 {} {}'.format(pat_entry.pat_l4_port, b))
|
||||
'10 50 {} {}'.format(port, bastion))
|
||||
|
||||
client.recordsets.create(zone['id'],
|
||||
'ssh.{}.{}'.format(hostname, project_id[:8]),
|
||||
'SRV', records)
|
||||
DESIGNATE.recordsets.create(ZONE['id'],
|
||||
'_ssh._tcp.{}.{}'.format(hostname,
|
||||
project_id[:8]),
|
||||
'SRV', records)
|
||||
|
||||
|
||||
_setup_zone()
|
||||
|
148
tatu/pat.py
148
tatu/pat.py
@ -10,81 +10,105 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from dragonflow.db import api_nb
|
||||
from dragonflow.db.models import l3
|
||||
from oslo_log import log
|
||||
from neutronclient.v2_0 import client
|
||||
from novaclient import client
|
||||
from dragonflow.db.models.core import Chassis
|
||||
from dragonflow.db.models.l2 import LogicalPort
|
||||
from dragonflow.db.models.l2 import LogicalRouter
|
||||
from dragonflow.db.models.l3 import PAT
|
||||
from dragonflow.db.models.l3 import PATEntry
|
||||
from oslo_log import log as logging
|
||||
import random
|
||||
from tatu.db import models as db
|
||||
from tatu import dns
|
||||
from tatu.config import CONF, NEUTRON, NOVA, DRAGONFLOW
|
||||
from tatu.db import models as tatu_db
|
||||
|
||||
# Need to load /etc/neutron/dragonflow.ini
|
||||
# config.init(sys.argv[1:])
|
||||
dragonflow = api_nb.NbApi.get_instance(False)
|
||||
LOG = logging.getLogger(__name__)
|
||||
PATS = DRAGONFLOW.get_all(PAT)
|
||||
|
||||
def add_pat():
|
||||
# First choose a host where the PAT will be bound.
|
||||
nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
|
||||
hosts = nova.servers.list()
|
||||
host_id = random.sample(hosts, 1)[0].id
|
||||
def _sync_pats():
|
||||
# TODO(pino): re-bind PATs when hypervisors fail (here and on notification)
|
||||
all_chassis = DRAGONFLOW.get_all(Chassis)
|
||||
# Filter the chassis that already have PATS assigned
|
||||
free_chassis = set(all_chassis).difference(p.chassis for p in PATS)
|
||||
# Don't make more PATs than there are free chassis
|
||||
num_to_make = min(CONF.tatu.num_total_pats - len(PATS),
|
||||
len(free_chassis))
|
||||
if num_to_make <= 0:
|
||||
return
|
||||
assigned_chassis = random.sample(free_chassis, num_to_make)
|
||||
for c in assigned_chassis:
|
||||
_add_pat(c)
|
||||
dns.sync_bastions(str(p.ip_address) for p in PATS)
|
||||
|
||||
# Now create the new port on the public network.
|
||||
neutron = client.Client(username=USER,
|
||||
password=PASS,
|
||||
project_name=PROJECT_NAME,
|
||||
auth_url=KEYSTONE_URL)
|
||||
|
||||
# Find the public network and allocate 2 ports.
|
||||
networks = neutron.list_networks(name='public')
|
||||
def _add_pat(chassis):
|
||||
# Find the public network and allocate a new port.
|
||||
networks = NEUTRON.list_networks(name='public')
|
||||
network_id = networks['networks'][0]['id']
|
||||
|
||||
body_value = {
|
||||
body = {
|
||||
"port": {
|
||||
"admin_state_up": True,
|
||||
"name": TatuPAT,
|
||||
"name": 'TatuPAT', # TODO(pino): set device owner to Tatu?
|
||||
"network_id": network_id,
|
||||
"binding: host_id": host_id
|
||||
}
|
||||
}
|
||||
pat_lport = neutron.create_port()
|
||||
# TODO: Bind the port to a specific host
|
||||
|
||||
pat = l3.PAT(
|
||||
topic = 'foo',
|
||||
ip_address = pat_lport.ip,
|
||||
lport = pat_lport
|
||||
neutron_port = NEUTRON.create_port(body)
|
||||
lport = DRAGONFLOW.get(LogicalPort(id=neutron_port['port']['id']))
|
||||
ip = get_ip4_from_lport(lport)
|
||||
pat = PAT(
|
||||
id = str(ip),
|
||||
topic = 'tatu', # TODO(pino): What topic? Admin project_id?
|
||||
ip_address = ip,
|
||||
lport = lport
|
||||
)
|
||||
dragonflow.create(pat)
|
||||
db.add_pat(pat.lport)
|
||||
return pat_lport.ip
|
||||
# We only need to store the PAT in dragonflow's DB, not API/MySQL
|
||||
DRAGONFLOW.create(pat)
|
||||
PATS.append(pat)
|
||||
|
||||
|
||||
# At startup, we create 1 PAT if none exists
|
||||
if not db.get_pats():
|
||||
add_pat()
|
||||
|
||||
# TODO(pino): need to re-bind PATs when hosts fail.
|
||||
def _get_ip4_from_lport(lport):
|
||||
for ip in lport.ips:
|
||||
if ip.version is 4:
|
||||
return ip
|
||||
return None
|
||||
|
||||
|
||||
def create_pat_entries(instance_id, fixed_l4_port, num=2):
|
||||
# TODO(pino): Use Neutron client to find a suitable lport on the instance
|
||||
lport = None
|
||||
lrouter = None
|
||||
# Reserve N assignments (i.e. IP:port pairs) on distinct IPs.
|
||||
pats = db.get_pats()
|
||||
pat_entries = set()
|
||||
if (num < len(pats)):
|
||||
pats = random.sample(pats, num_assignments)
|
||||
for pat in pats:
|
||||
pat_l4_port = db.reserve_l4_port(pat.ip, lport.id, lport.ip, fixed_l4_port)
|
||||
pat_entry = l3.PATEntry(
|
||||
pat = pat,
|
||||
pat_l4_port = pat_l4_port,
|
||||
fixed_ip_address = lport.ip,
|
||||
fixed_l4_port = fixed_l4_port,
|
||||
lport = lport,
|
||||
lrouter = df_fields.ReferenceField(LogicalRouter),
|
||||
)
|
||||
dragonflow.create(pat_entry)
|
||||
pat_entries.add(pat_entry)
|
||||
return pat_entries
|
||||
def df_find_lrouter_by_lport(lport):
|
||||
lrouters = DRAGONFLOW.get_all(LogicalRouter)
|
||||
for lr in lrouters:
|
||||
for lp in lr.ports:
|
||||
if lp.lswitch.id == lport.lswitch.id:
|
||||
return lr
|
||||
return None
|
||||
|
||||
def create_pat_entries(sql_session, instance_id, fixed_l4_port,
|
||||
num=CONF.tatu.num_pat_bastions_per_server):
|
||||
port_ip_tuples = []
|
||||
server = NOVA.servers.get(instance_id)
|
||||
ifaces = server.interface_list()
|
||||
for iface in ifaces:
|
||||
lport = DRAGONFLOW.get(LogicalPort(id=iface['port_id']))
|
||||
# TODO(pino): no router? consider SNAT of source IP to 169.254.169.254
|
||||
lrouter = df_find_lrouter_by_lport(lport)
|
||||
if lrouter is None: continue
|
||||
# Reserve N l4 ports on distinct IPs.
|
||||
pats = PATS
|
||||
if (num < len(PATS)):
|
||||
pats = random.sample(pats, num)
|
||||
for pat in pats:
|
||||
pat_l4_port = tatu_db.reserve_l4_port(sql_session, str(pat.ip))
|
||||
pat_entry = PATEntry(
|
||||
pat = pat,
|
||||
pat_l4_port = pat_l4_port,
|
||||
fixed_ip_address = _get_ip4_from_lport(lport),
|
||||
fixed_l4_port = fixed_l4_port,
|
||||
lport = lport,
|
||||
lrouter = df_fields.ReferenceField(LogicalRouter),
|
||||
)
|
||||
DRAGONFLOW.create(pat_entry)
|
||||
port_ip_tuples.append((pat_l4_port, str(pat.ip)))
|
||||
# if we got here, we now have the required pat_entries
|
||||
break
|
||||
return port_ip_tuples
|
||||
|
||||
|
||||
_sync_pats()
|
@ -13,8 +13,8 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from tempfile import mkdtemp
|
||||
import uuid
|
||||
from tempfile import mkdtemp
|
||||
|
||||
|
||||
def random_uuid():
|
||||
@ -24,7 +24,6 @@ def random_uuid():
|
||||
def generateCert(auth_key, entity_key, hostname=None, principals='root'):
|
||||
# Temporarily write the authority private key, entity public key to files
|
||||
prefix = uuid.uuid4().hex
|
||||
# Todo: make the temporary directory configurable or secure it.
|
||||
temp_dir = mkdtemp()
|
||||
ca_file = '/'.join([temp_dir, 'ca_key'])
|
||||
pub_file = '/'.join([temp_dir, 'entity.pub'])
|
||||
|
Loading…
Reference in New Issue
Block a user