Draft bastion support and Designate integration.

This commit is contained in:
Pino de Candia 2017-12-29 03:53:32 -06:00
parent 91c0b33338
commit 27b180f864
5 changed files with 181 additions and 1 deletions

View File

@ -18,6 +18,10 @@ pyramid>=1.9.1 # BSD-derived (http://www.repoze.org/LICENSE.txt)
Paste # MIT
dogpile.cache
python-memcached
python-designateclient
python-neutronclient
python-novaclient
dragonflow
oslo_concurrency
eventlet
vine

View File

@ -10,11 +10,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from Crypto.PublicKey import RSA
import falcon
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.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
@ -225,3 +233,11 @@ class NovaVendorData(object):
resp.body = json.dumps(vendordata)
resp.location = '/hosttokens/' + token.token_id
resp.status = falcon.HTTP_201
# 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)

67
tatu/dns.py Normal file
View File

@ -0,0 +1,67 @@
# 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 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
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 = {}
def setup(bastions=[]):
# TODO: retrieve the zone name and email from configuration
try:
global zone
zone = client.zones.create('julia.com.', email='pino@yahoo.com')
except Conflict:
pass
# TODO: fetch all existing bastions
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 add_srv_records(project_id, hostname, pat_entries):
records = []
for pat_entry in pat_entries:
b = bastions[pat_entries.pat.ip_address]
# SRV record format is: priority weight port A-name
records.add(
'10 50 {} {}'.format(pat_entry.pat_l4_port, b))
client.recordsets.create(zone['id'],
'ssh.{}.{}'.format(hostname, project_id[:8]),
'SRV', records)

View File

@ -62,6 +62,9 @@ class NotificationEndpoint(object):
LOG.error("Status update or unknown")
# TODO(pino): listen to host delete notifications, clean up PATs and DNS
# TODO(pino): Listen to user deletions and revoke their certs
def main():
transport = oslo_messaging.get_notification_transport(cfg.CONF)
targets = [oslo_messaging.Target(topic='notifications')]

90
tatu/pat.py Normal file
View File

@ -0,0 +1,90 @@
# 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.
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
import random
from tatu.db import models as db
# Need to load /etc/neutron/dragonflow.ini
# config.init(sys.argv[1:])
dragonflow = api_nb.NbApi.get_instance(False)
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
# 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')
network_id = networks['networks'][0]['id']
body_value = {
"port": {
"admin_state_up": True,
"name": TatuPAT,
"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
)
dragonflow.create(pat)
db.add_pat(pat.lport)
return pat_lport.ip
# 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 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