diff --git a/requirements.txt b/requirements.txt index d52b2ec..254b5ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tatu/api/models.py b/tatu/api/models.py index a82c577..78b8a00 100644 --- a/tatu/api/models.py +++ b/tatu/api/models.py @@ -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) diff --git a/tatu/dns.py b/tatu/dns.py new file mode 100644 index 0000000..ed6592e --- /dev/null +++ b/tatu/dns.py @@ -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) diff --git a/tatu/notifications.py b/tatu/notifications.py index 8783461..0e53f3b 100644 --- a/tatu/notifications.py +++ b/tatu/notifications.py @@ -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')] diff --git a/tatu/pat.py b/tatu/pat.py new file mode 100644 index 0000000..8b9a244 --- /dev/null +++ b/tatu/pat.py @@ -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