398 lines
15 KiB
Python
398 lines
15 KiB
Python
# 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 flask import jsonify
|
|
from flask import request
|
|
from jsonschema import validate
|
|
import netaddr
|
|
from neutronclient.common import exceptions as n_exceptions
|
|
|
|
from kuryr import app
|
|
from kuryr.constants import SCHEMA
|
|
from kuryr import exceptions
|
|
from kuryr import schemata
|
|
from kuryr import utils
|
|
|
|
|
|
# TODO(tfukushima): Retrieve configuration info from a config file.
|
|
OS_URL = os.environ.get('OS_URL', 'http://127.0.0.1:9696/')
|
|
OS_TOKEN = os.environ.get('OS_TOKEN', '9999888877776666')
|
|
OS_AUTH_URL = os.environ.get('OS_AUTH_URL', 'https://127.0.0.1:5000/v2.0/')
|
|
OS_USERNAME = os.environ.get('OS_USERNAME', '')
|
|
OS_PASSWORD = os.environ.get('OS_PASSWORD', '')
|
|
OS_TENANT_NAME = os.environ.get('OS_TENANT_NAME', '')
|
|
|
|
if OS_USERNAME and OS_PASSWORD:
|
|
# Authenticate with password crentials
|
|
app.neutron = utils.get_neutron_client(
|
|
url=OS_URL, username=OS_USERNAME, tenant_name=OS_TENANT_NAME,
|
|
password=OS_PASSWORD, auth_url=OS_AUTH_URL)
|
|
else:
|
|
app.neutron = utils.get_neutron_client_simple(url=OS_URL, token=OS_TOKEN)
|
|
|
|
# TODO(tfukushima): Retrieve the following subnet names from the config file.
|
|
SUBNET_POOLS_V4 = [
|
|
p.strip() for p in os.environ.get('SUBNET_POOLS_V4', 'kuryr').split(',')]
|
|
SUBNET_POOLS_V6 = [
|
|
p.strip() for p in os.environ.get('SUBNET_POOLS_V6', 'kuryr6').split(',')]
|
|
|
|
app.neutron.format = 'json'
|
|
|
|
|
|
def _get_subnets_by_attrs(**attrs):
|
|
subnets = app.neutron.list_subnets(**attrs)
|
|
if len(subnets) > 1:
|
|
raise exceptions.DuplicatedResourceException(
|
|
"Multiple Neutron subnets exist for the params {0} "
|
|
.format(', '.join(['{0}={1}'.format(k, v)
|
|
for k, v in attrs.items()])))
|
|
return subnets['subnets']
|
|
|
|
|
|
def _handle_allocation_from_pools(neutron_network_id, existing_subnets):
|
|
for v4_subnet_name in SUBNET_POOLS_V4:
|
|
v4_subnets = _get_subnets_by_attrs(
|
|
network_id=neutron_network_id, name=v4_subnet_name)
|
|
existing_subnets += v4_subnets
|
|
|
|
for v6_subnet_name in SUBNET_POOLS_V6:
|
|
v6_subnets = _get_subnets_by_attrs(
|
|
network_id=neutron_network_id, name=v6_subnet_name)
|
|
existing_subnets += v6_subnets
|
|
|
|
|
|
def _process_subnet(neutron_network_id, endpoint_id, interface_cidr,
|
|
new_subnets, existing_subnets):
|
|
subnets = _get_subnets_by_attrs(
|
|
network_id=neutron_network_id, cidr=interface_cidr)
|
|
if subnets:
|
|
existing_subnets += subnets
|
|
else:
|
|
cidr = netaddr.IPNetwork(interface_cidr)
|
|
subnet_network = str(cidr.network)
|
|
subnet_cidr = '/'.join([subnet_network,
|
|
str(cidr.prefixlen)])
|
|
new_subnets.append({
|
|
'name': '-'.join([endpoint_id, subnet_network]),
|
|
# Allocate all IP addresses in the subnet.
|
|
'allocation_pools': None,
|
|
'network_id': neutron_network_id,
|
|
'ip_version': cidr.version,
|
|
'cidr': subnet_cidr,
|
|
})
|
|
|
|
|
|
def _handle_explicit_allocation(neutron_network_id, endpoint_id,
|
|
interface_cidrv4, interface_cidrv6,
|
|
new_subnets, existing_subnets):
|
|
if interface_cidrv4:
|
|
_process_subnet(neutron_network_id, endpoint_id, interface_cidrv4,
|
|
new_subnets, existing_subnets)
|
|
|
|
if interface_cidrv6:
|
|
_process_subnet(neutron_network_id, endpoint_id, interface_cidrv6,
|
|
new_subnets, existing_subnets)
|
|
|
|
if new_subnets:
|
|
# Bulk create operation of subnets
|
|
created_subnets = app.neutron.create_subnet({'subnets': new_subnets})
|
|
|
|
return created_subnets
|
|
|
|
|
|
def _create_subnets_and_or_port(interfaces, neutron_network_id, endpoint_id):
|
|
response_interfaces = []
|
|
for interface in interfaces:
|
|
existing_subnets = []
|
|
created_subnets = {}
|
|
# v4 and v6 Subnets for bulk creation.
|
|
new_subnets = []
|
|
|
|
interface_id = interface['ID']
|
|
interface_cidrv4 = interface.get('Address', '')
|
|
interface_cidrv6 = interface.get('AddressIPv6', '')
|
|
interface_mac = interface['MacAddress']
|
|
|
|
if interface_cidrv4 or interface_cidrv6:
|
|
created_subnets = _handle_explicit_allocation(
|
|
neutron_network_id, endpoint_id, interface_cidrv4,
|
|
interface_cidrv6, new_subnets, existing_subnets)
|
|
else:
|
|
_handle_allocation_from_pools(
|
|
neutron_network_id, existing_subnets)
|
|
|
|
try:
|
|
port = {
|
|
'name': '-'.join([endpoint_id, str(interface_id), 'port']),
|
|
'admin_state_up': True,
|
|
'mac_address': interface_mac,
|
|
'network_id': neutron_network_id,
|
|
}
|
|
created_subnets = created_subnets.get('subnets', [])
|
|
all_subnets = created_subnets + existing_subnets
|
|
fixed_ips = port['fixed_ips'] = []
|
|
for subnet in all_subnets:
|
|
fixed_ip = {'subnet_id': subnet['id']}
|
|
if subnet['ip_version'] == 4:
|
|
cidr = netaddr.IPNetwork(interface_cidrv4)
|
|
else:
|
|
cidr = netaddr.IPNetwork(interface_cidrv6)
|
|
subnet_cidr = '/'.join([str(cidr.network),
|
|
str(cidr.prefixlen)])
|
|
if subnet['cidr'] != subnet_cidr:
|
|
continue
|
|
fixed_ip['ip_address'] = str(cidr.ip)
|
|
fixed_ips.append(fixed_ip)
|
|
app.neutron.create_port({'port': port})
|
|
|
|
response_interfaces.append({
|
|
'ID': interface_id,
|
|
'Address': interface_cidrv4,
|
|
'AddressIPv6': interface_cidrv6,
|
|
'MacAddress': interface_mac
|
|
})
|
|
except n_exceptions.NeutronClientException as ex:
|
|
app.logger.error("Error happend during creating a "
|
|
"Neutron port: {0}".format(ex))
|
|
# Rollback the subnets creation
|
|
for subnet in created_subnets:
|
|
app.neutron.delete_subnet(subnet['id'])
|
|
raise
|
|
|
|
return response_interfaces
|
|
|
|
|
|
@app.route('/Plugin.Activate', methods=['POST'])
|
|
def plugin_activate():
|
|
return jsonify(SCHEMA['PLUGIN_ACTIVATE'])
|
|
|
|
|
|
@app.route('/NetworkDriver.CreateNetwork', methods=['POST'])
|
|
def network_driver_create_network():
|
|
"""Creates a new Neutron Network which name is the given NetworkID.
|
|
|
|
This function takes the following JSON data and delegates the actual
|
|
network creation to the Neutron client. libnetwork's NetworkID is used as
|
|
the name of Network in Neutron. ::
|
|
|
|
{
|
|
"NetworkID": string,
|
|
"Options": {
|
|
...
|
|
}
|
|
}
|
|
|
|
See the following link for more details about the spec:
|
|
|
|
https://github.com/docker/libnetwork/blob/master/docs/remote.md#create-network # noqa
|
|
"""
|
|
json_data = request.get_json(force=True)
|
|
app.logger.debug("Received JSON data {0} for /NetworkDriver.CreateNetwork"
|
|
.format(json_data))
|
|
validate(json_data, schemata.NETWORK_CREATE_SCHEMA)
|
|
|
|
neutron_network_name = json_data['NetworkID']
|
|
|
|
network = app.neutron.create_network(
|
|
{'network': {'name': neutron_network_name, "admin_state_up": True}})
|
|
|
|
app.logger.info("Created a new network with name {0} successfully: {1}"
|
|
.format(neutron_network_name, network))
|
|
return jsonify(SCHEMA['SUCCESS'])
|
|
|
|
|
|
@app.route('/NetworkDriver.DeleteNetwork', methods=['POST'])
|
|
def network_driver_delete_network():
|
|
"""Deletes the Neutron Network which name is the given NetworkID.
|
|
|
|
This function takes the following JSON data and delegates the actual
|
|
network deletion to the Neutron client. ::
|
|
|
|
{
|
|
"NetworkID": string
|
|
}
|
|
|
|
See the following link for more details about the spec:
|
|
|
|
https://github.com/docker/libnetwork/blob/master/docs/remote.md#delete-network # noqa
|
|
"""
|
|
json_data = request.get_json(force=True)
|
|
|
|
app.logger.debug("Received JSON data {0} for /NetworkDriver.DeleteNetwork"
|
|
.format(json_data))
|
|
# TODO(tfukushima): Add a validation of the JSON data for the network.
|
|
neutron_network_name = json_data['NetworkID']
|
|
|
|
filtered_networks = app.neutron.list_networks(name=neutron_network_name)
|
|
|
|
# We assume Neutron's Network names are not conflicted in Kuryr because
|
|
# they are Docker IDs, 256 bits hashed values, which are rarely conflicted.
|
|
# However, if there're multiple networks associated with the single
|
|
# NetworkID, it raises DuplicatedResourceException and stops processes.
|
|
# See the following doc for more details about Docker's IDs:
|
|
# https://github.com/docker/docker/blob/master/docs/terms/container.md#container-ids # noqa
|
|
if len(filtered_networks) > 1:
|
|
raise exceptions.DuplicatedResourceException(
|
|
"Multiple Neutron Networks exist for NetworkID {0}"
|
|
.format(neutron_network_name))
|
|
else:
|
|
neutron_network_id = filtered_networks['networks'][0]['id']
|
|
app.neutron.delete_network(neutron_network_id)
|
|
app.logger.info("Deleted the network with ID {0} successfully"
|
|
.format(neutron_network_id))
|
|
return jsonify(SCHEMA['SUCCESS'])
|
|
|
|
|
|
@app.route('/NetworkDriver.CreateEndpoint', methods=['POST'])
|
|
def network_driver_create_endpoint():
|
|
"""Creates new Neutron Subnets and a Port with the given EndpointID.
|
|
|
|
This function takes the following JSON data and delegates the actual
|
|
endpoint creation to the Neutron client mapping it into Subnet and Port. ::
|
|
|
|
{
|
|
"NetworkID": string,
|
|
"EndpointID": string,
|
|
"Options": {
|
|
...
|
|
},
|
|
"Interfaces": [{
|
|
"ID": int,
|
|
"Address": string,
|
|
"AddressIPv6": string,
|
|
"MacAddress": string
|
|
}, ...]
|
|
}
|
|
|
|
See the following link for more details about the spec:
|
|
|
|
https://github.com/docker/libnetwork/blob/master/docs/remote.md#create-endpoint # noqa
|
|
"""
|
|
json_data = request.get_json(force=True)
|
|
|
|
app.logger.debug("Received JSON data {0} for /NetworkDriver.CreateEndpoint"
|
|
.format(json_data))
|
|
# TODO(tfukushima): Add a validation of the JSON data for the subnet.
|
|
neutron_network_name = json_data['NetworkID']
|
|
endpoint_id = json_data['EndpointID']
|
|
|
|
filtered_networks = app.neutron.list_networks(name=neutron_network_name)
|
|
|
|
if not filtered_networks:
|
|
return jsonify({
|
|
'Err': "Neutron network associated with ID {0} doesn't exit."
|
|
.format(neutron_network_name)
|
|
})
|
|
elif len(filtered_networks) > 1:
|
|
raise exceptions.DuplicatedResourceException(
|
|
"Multiple Neutron Networks exist for NetworkID {0}"
|
|
.format(neutron_network_name))
|
|
else:
|
|
neutron_network_id = filtered_networks['networks'][0]['id']
|
|
interfaces = json_data['Interfaces']
|
|
response_interfaces = _create_subnets_and_or_port(
|
|
interfaces, neutron_network_id, endpoint_id)
|
|
|
|
return jsonify({'Interfaces': response_interfaces})
|
|
|
|
|
|
@app.route('/NetworkDriver.EndpointOperInfo', methods=['POST'])
|
|
def network_driver_endpoint_operational_info():
|
|
return jsonify(SCHEMA['ENDPOINT_OPER_INFO'])
|
|
|
|
|
|
@app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
|
|
def network_driver_delete_endpoint():
|
|
"""Deletes Neutron Subnets and a Port with the given EndpointID.
|
|
|
|
This function takes the following JSON data and delegates the actual
|
|
endpoint deletion to the Neutron client mapping it into Subnet and Port. ::
|
|
|
|
{
|
|
"NetworkID": string,
|
|
"EndpointID": string
|
|
}
|
|
|
|
See the following link for more details about the spec:
|
|
|
|
https://github.com/docker/libnetwork/blob/master/docs/remote.md#delete-endpoint # noqa
|
|
"""
|
|
json_data = request.get_json(force=True)
|
|
# TODO(tfukushima): Add a validation of the JSON data for the subnet.
|
|
app.logger.debug("Received JSON data {0} for /NetworkDriver.DeleteEndpoint"
|
|
.format(json_data))
|
|
|
|
neutron_network_name = json_data['NetworkID']
|
|
endpoint_id = json_data['EndpointID']
|
|
|
|
filtered_networks = app.neutron.list_networks(name=neutron_network_name)
|
|
|
|
if not filtered_networks:
|
|
return jsonify({
|
|
'Err': "Neutron network associated with ID {0} doesn't exit."
|
|
.format(neutron_network_name)
|
|
})
|
|
elif len(filtered_networks) > 1:
|
|
raise exceptions.DuplicatedResourceException(
|
|
"Multiple Neutron Networks exist for NetworkID {0}"
|
|
.format(neutron_network_name))
|
|
else:
|
|
neutron_network_id = filtered_networks['networks'][0]['id']
|
|
filtered_ports = []
|
|
concerned_subnet_ids = []
|
|
try:
|
|
filtered_ports = app.neutron.list_ports(
|
|
network_id=neutron_network_id)
|
|
filtered_ports = [port for port in filtered_ports['ports']
|
|
if endpoint_id in port['name']]
|
|
for port in filtered_ports:
|
|
fixed_ips = port.get('fixed_ips', [])
|
|
for fixed_ip in fixed_ips:
|
|
concerned_subnet_ids.append(fixed_ip['subnet_id'])
|
|
app.neutron.delete_port(port['id'])
|
|
except n_exceptions.NeutronClientException as ex:
|
|
app.logger.error("Error happend during deleting a "
|
|
"Neutron ports: {0}".format(ex))
|
|
raise
|
|
|
|
for subnet_id in concerned_subnet_ids:
|
|
# If the subnet to be deleted has any port, when some ports are
|
|
# referring to the subnets in other words, delete_subnet throws an
|
|
# exception, SubnetInUse that extends Conflict. This can happen
|
|
# when the multiple Docker endpoints are created with the same
|
|
# subnet CIDR and it's totally the normal case. So we'd just log
|
|
# that and continue to proceed.
|
|
try:
|
|
app.neutron.delete_subnet(subnet_id)
|
|
except n_exceptions.Conflict as ex:
|
|
app.logger.info("The subnet with ID {0} is still referred "
|
|
"from other ports and it can't be deleted for "
|
|
"now.".format(subnet_id))
|
|
except n_exceptions.NeutronClientException as ex:
|
|
app.logger.error("Error happend during deleting a "
|
|
"Neutron subnets: {0}".format(ex))
|
|
raise
|
|
|
|
return jsonify(SCHEMA['SUCCESS'])
|
|
|
|
|
|
@app.route('/NetworkDriver.Join', methods=['POST'])
|
|
def network_driver_join():
|
|
return jsonify(SCHEMA['JOIN'])
|
|
|
|
|
|
@app.route('/NetworkDriver.Leave', methods=['POST'])
|
|
def network_driver_leave():
|
|
return jsonify(SCHEMA['SUCCESS'])
|