Extend-a-network

Create a new extension to the proxy, which will allow networks to be
extended across clouds.

Additionally, provide lots of documentation for this new feature.

Change-Id: I9088e3509f71fb363ddc7f504cbb96f94932cc1e
This commit is contained in:
Jeremy Freudberg 2017-10-06 23:12:31 +00:00
parent 59af61ff67
commit 955f28d397
9 changed files with 365 additions and 7 deletions

View File

@ -19,6 +19,7 @@ Operators Guide
installation installation
identity identity
volumes volumes
network-fed
* `Release Notes <https://mixmatch.readthedocs.org/projects/releasenotes>`_ * `Release Notes <https://mixmatch.readthedocs.org/projects/releasenotes>`_

223
doc/source/network-fed.rst Normal file
View File

@ -0,0 +1,223 @@
==================
Network Federation
==================
What is meant by "network federation"?
======================================
Mixmatch offers a mechanism to extend a network across clouds. Note that this
idea of 'extending' is different than the direct access which the image
federation and volume federation features offer. A user's choice to extend a
network will usually be explicit and voluntary, whereas the sharing of images
and volumes tends towards being implicit and automatic.
Support for network federation requires that Neutron be backed by the ML2
plugin. This plugin is often considered normal, or vanilla, so most clouds
probably satisfy this requirement easily.
The precise mechanism which allows the network federation feature to function
is VXLAN tunneling between clouds.
Finally, note that currently the scope of this feature is limited to extending
networks from a remote cloud to the so-called 'local' cloud in which the
Mixmatch proxy service resides.
Network federation for operators
================================
Some steps must be taken to configure clouds in such a way that the network
federation feature works as intended.
Registering remote VXLAN endpoints
----------------------------------
In a single-cloud deployment, the Neutron ML2 plugin creates a VXLAN mesh
among compute nodes, to allow virtual machines residing on separate physical
hardware to communicate.
The ability to manipulate the VXLAN mesh is not exposed by the Neutron API, so
operators must edit database entries manually. Below, we use MySQL as an
example, but operators should take care to translate these queries to be
compatibile with their own database.
Below is how the database entries may appear for a single-cloud deployment:
.. sourcecode:: console
mysql> select * from neutron.ml2_vxlan_endpoints;
+-------------+----------+------------+
| ip_address | udp_port | host |
+-------------+----------+------------+
| 10.19.97.20 | 4789 | compute-01 |
| 10.19.97.21 | 4789 | controller |
| 10.19.97.22 | 4789 | compute-02 |
+-------------+----------+------------+
3 rows in set (0.00 sec)
..
These entries are automatically populated by Neutron and contain references to
each compute node in the cloud.
In order to allow networks to extend across clouds, operators should simply
insert entries for the compute nodes in remote clouds:
.. sourcecode:: console
mysql> insert into neutron.ml2_vxlan_endpoints (ip_address, udp_port, host) values ('129.10.5.10', 4789, 'compute-01.remotecloud.org');
Query OK, 1 row affected (0.00 sec)
mysql> select * from neutron.ml2_vxlan_endpoints;
+-------------+----------+----------------------------+
| ip_address | udp_port | host |
+-------------+----------+----------------------------+
| 10.19.97.20 | 4789 | compute-01 |
| 10.19.97.21 | 4789 | controller |
| 10.19.97.22 | 4789 | compute-02 |
| 129.10.5.10 | 4789 | compute-01.remotecloud.org |
+-------------+----------+----------------------------+
4 rows in set (0.00 sec)
..
Finally, operators should take care to ensure that the incoming UDP traffic on
port 4789 is in-fact permitted.
**NOTE**: Similar steps to those above must be performed on each cloud, in
order to support bidirectional traffic.
Because managing numerous entries in the database can become unwieldy, an
operator might consider installing some device, of an unknown nature, which
could perform VXLAN termination for an entire cloud. A reference to this
device would appear in the database instead of entries for each compute node.
Configuring Neutron policies
----------------------------
The operations which Mixmatch performs to extend a network are, by default and
by nature, privileged operations. The default Neutron policy restricts the
performance of these operations to users with the ``admin`` role. Therefore, in
its home cloud only the Mixmatch service user should have this role.
In a federation of clouds, however, the landlord of each remote cloud will
probably not want to give out this ``admin`` role. In fact he or she will want
to only give the Mixmatch service user the minimal amount of elevated
permissions needed to perform the network-extending operations, and no more.
Therefore a new role, which we will call ``mixmatch_fancy_role``, should be
created in each remote cloud. Operators should ensure that the Mixmatch service
user is given this role in its mapped projects in those remote clouds. Then,
the following entries in the Neutron ``policy.json`` file should be changed or
added: (at the time of writing Neutron still does not have any default policies
registered in code, so the rest of the policy file must stay intact)
.. sourcecode:: json
{
"mixmatch": "role:mixmatch_fancy_role",
"context_is_advsvc": "rule:mixmatch",
"get_network:provider:segmentation_id": "rule:admin_only or rule:mixmatch"
}
..
Note that due to limitations in Neutron's policy engine we must take advantage
of the ``advsvc`` ("Advanced Services") permission feature, rather than define
our own custom policy. Therefore, operators might want to additionally tweak
the other default entries in policy.json which reference this role (mostly
related to port operations).
Ensuring non-conflicting VXLAN IDs
----------------------------------
Because Mixmatch will be creating new networks with a particular VXLAN ID
specified, there may be conflicts if the various remote clouds assign these
IDs randomly (the default behavior). In the
``/etc/neutron/plugins/ml2/ml2_conf.ini`` file of each cloud, operators should
take care to set a reasonable and non-overlapping ``start:end`` value for
``[ml2_type_vxlan]/vni_ranges``.
Network federation for users
============================
Users consume the network federation feature by sending requests to an
extension of the Neutron API which is exposed by the Mixmatch proxy service.
API reference
-------------
The details of that API call follow below. (Note that because the network
extending is always performed as remote-to-local, the ``MM-SERVICE-PROVIDER``
header is not understood by this call.)
.. sourcecode:: console
POST <mixmatch url>/network/v2.0/networks/extended
..
.. sourcecode:: json
{
"network": {
"existing_net_id": "60ed86b2-8db8-4459-8d31-475345534dec",
"existing_net_sp": "some_remote_sp",
"name": "my_cool_extended_network"
}
}
..
On success, the response of this API call will be identical in format to the
standard Neutron POST ``/v2.0/networks``. On failure, there are several
specific error codes which can be returned:
* 400, if ``existing_net_id`` or ``existing_net_sp`` are not present in the
request body
* 401, if the user is unauthorized (no token or invalid token)
* 409, if there is a naming conflict for the extended network
* 422, if a request to Neutron ended with a client-side error (usually network
not found or not available to the user), or if the service provider is not
known to Mixmatch
* 503, if a request to Neutron ended with a server-side error
Subnet management
-----------------
Note however, that it will remain the responsibility of the user to manage
the subnets of extended networks. In other words, the network-extending
functionality which Mixmatch exposes does not perform any subnet operations.
Users should take care to make sure that for the subnet in each cloud, the
first three octets of the (IPv4) subnet are the same, but that the allocation
pools do not overlap. Additionally, the user should ensure that DHCP is only
enabled for the subnet of one cloud and not the other. (The choice of which
subnet will offer DHCP can, in practice, be an arbitrary one.) Users can have
the two subnets share one router ("gateway"), or have a separate gateway for
each cloud.
Some example code which may help in following these guidelines is found below:
.. sourcecode:: console
old_subnet = (
[s for s in CLOUD1_NEUTRON_CLIENT.list_subnets()['subnets']
if (s['ip_version'] == 4 and
s['network_id'] == CLOUD1_NETWORK_ID)][0]
)
old_subnet_id = old_subnet['id']
old_subnet_start = old_subnet['allocation_pools'][0]['start']
maximum_ip = int(
old_subnet['allocation_pools'][0]['end']
.split('.')[-1]
)
pool_base = re.sub(r'\d+$', '', old_subnet_start)
CLOUD1_NEUTRON_CLIENT.update_subnet(
old_subnet_id, body={'subnet': {'allocation_pools':
[{'start': old_subnet_start,
'end': '{}{}'.format(
pool_base, maximum_ip // 2)}]}}
)
new_subnet_body = (
{'enable_dhcp': False,
'network_id': CLOUD2_NETWORK_ID,
'dns_nameservers': old_subnet['dns_nameservers'],
'ip_version': 4,
'gateway_ip': old_subnet['gateway_ip'],
'cidr': old_subnet['cidr'],
'allocation_pools':
[{'start': '{}{}'.format(pool_base, maximum_ip // 2 + 1),
'end': '{}{}'.format(pool_base, maximum_ip)}]
}
)
new_subnet = CLOUD2_NEUTRON_CLIENT.create_subnet(
body={'subnet': new_subnet_body})
..

View File

@ -77,6 +77,7 @@ python-dateutil==2.7.0
python-editor==1.0.3 python-editor==1.0.3
python-keystoneclient==3.8.0 python-keystoneclient==3.8.0
python-mimeparse==1.6.0 python-mimeparse==1.6.0
python-neutronclient==6.7.0
python-subunit==1.0.0 python-subunit==1.0.0
pytz==2018.3 pytz==2018.3
PyYAML==3.12 PyYAML==3.12

View File

@ -30,9 +30,9 @@ MEMOIZE_SESSION = config.auth.MEMOIZE
@MEMOIZE_SESSION @MEMOIZE_SESSION
def get_client(): def get_admin_session(sp=None):
"""Return a Keystone client capable of validating tokens.""" """Return a Keystone session using admin service credentials."""
LOG.info("Getting Admin Client") LOG.info("Getting Admin Session")
service_auth = identity.Password( service_auth = identity.Password(
auth_url=CONF.auth.auth_url, auth_url=CONF.auth.auth_url,
username=CONF.auth.username, username=CONF.auth.username,
@ -41,15 +41,29 @@ def get_client():
project_domain_id=CONF.auth.project_domain_id, project_domain_id=CONF.auth.project_domain_id,
user_domain_id=CONF.auth.user_domain_id user_domain_id=CONF.auth.user_domain_id
) )
local_session = session.Session(auth=service_auth) sess = session.Session(auth=service_auth)
return v3.client.Client(session=local_session) if sp is None:
return sess
else:
token = sess.get_token()
project_id = get_projects_at_sp(sp, token)[0]
remote_admin_sess = get_sp_auth(sp, token, project_id)
return remote_admin_sess
@MEMOIZE_SESSION
def get_client(session):
"""Return a client object given a session object."""
LOG.debug("Getting client for %s" % session)
return v3.client.Client(session=session)
@MEMOIZE_SESSION @MEMOIZE_SESSION
def get_local_auth(user_token): def get_local_auth(user_token):
"""Return a Keystone session for the local cluster.""" """Return a Keystone session for the local cluster."""
LOG.debug("Getting session for %s" % user_token) LOG.debug("Getting session for %s" % user_token)
client = get_client() admin_session = get_admin_session()
client = get_client(admin_session)
token = v3.tokens.TokenManager(client) token = v3.tokens.TokenManager(client)
try: try:

View File

@ -37,3 +37,12 @@ class Extension(object):
def handle_response(self, response): def handle_response(self, response):
pass pass
class FinalResponse(object):
stream = False
def __init__(self, text, status_code, headers):
self.text = text
self.status_code = status_code
self.headers = headers

View File

@ -0,0 +1,103 @@
# Copyright 2017 Massachusetts Open Cloud
#
# 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 mixmatch import auth
from mixmatch.config import CONF
from mixmatch.extend import base
from mixmatch import utils
import flask
from neutronclient.v2_0 import client as neutron
from neutronclient.common import exceptions as n_ex
from oslo_serialization import jsonutils
class ExtendNetwork(base.Extension):
"""An extension which smells like Neutron's POST /networks.
It extends networks by matching up VXLAN IDs.
"""
ROUTES = [
('/network/v2.0/networks/extended', ['POST']),
# For now, mask Neutron POST /networks. Later, move the extend-network
# logic into a new, separate API.
]
@staticmethod
def _has_access(net_id, remote_project_ids, origin_sp, user_tok):
for remote_project_id in remote_project_ids:
sp_sess = auth.get_sp_auth(origin_sp, user_tok, remote_project_id)
remote_user_client = neutron.Client(session=sp_sess)
try:
remote_user_client.show_network(net_id)
return True
except n_ex.NeutronClientException as e:
if e.status_code < 500:
continue
else:
flask.abort(503)
return False
def handle_request(self, request):
body = jsonutils.loads(request.body)
origin_sp = utils.safe_pop(body['network'], 'existing_net_sp')
existing_net_id = utils.safe_pop(body['network'], 'existing_net_id')
user_tok = request.token
if origin_sp is None or existing_net_id is None:
flask.abort(400)
if origin_sp not in CONF.service_providers:
flask.abort(422)
remote_admin_sess = auth.get_admin_session(origin_sp)
remote_admin_neutronclient = neutron.Client(session=remote_admin_sess)
try:
original = (
remote_admin_neutronclient.show_network(existing_net_id)
)
except n_ex.NeutronClientException as e:
flask.abort(422 if e.status_code < 500 else 503)
remote_project_ids = auth.get_projects_at_sp(origin_sp, user_tok)
if not self._has_access(existing_net_id, remote_project_ids,
origin_sp, user_tok):
flask.abort(422)
local_admin_session = auth.get_admin_session()
local_admin_neutronclient = (
neutron.Client(session=local_admin_session)
)
body['network']['provider:network_type'] = 'vxlan'
vxlan_id = original['network']['provider:segmentation_id']
body['network']['provider:segmentation_id'] = vxlan_id
local_project_id = auth.get_local_auth(user_tok).get_project_id()
body['network']['project_id'] = local_project_id
try:
new_net = local_admin_neutronclient.create_network(body)
except n_ex.Conflict:
# Conflict could happen when names collide. So, give client error.
flask.abort(409)
except n_ex.NeutronClientException:
flask.abort(503)
return base.FinalResponse(
jsonutils.dumps(new_net),
201,
headers={'Content-Type': 'application/json'}
)

View File

@ -18,6 +18,7 @@ import requests
from urllib3.util import retry from urllib3.util import retry
import flask import flask
from flask import abort from flask import abort
import functools
from mixmatch import config from mixmatch import config
from mixmatch.config import LOG, CONF, service_providers from mixmatch.config import LOG, CONF, service_providers
@ -117,8 +118,12 @@ class RequestHandler(object):
self.append_proxy(self.details.headers) self.append_proxy(self.details.headers)
# TODO(jfreud): more sophisticated/ordered invocation of extensions
for extension in self.extensions: for extension in self.extensions:
extension.handle_request(self.details) out = extension.handle_request(self.details)
if out is not None:
self._forward = functools.partial(self._finalize, out)
return
if not self.details.version: if not self.details.version:
if CONF.aggregation: if CONF.aggregation:

View File

@ -15,6 +15,7 @@ oslo.db>=4.27.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
requests>=2.14.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0
six>=1.10.0 # MIT six>=1.10.0 # MIT
stevedore>=1.20.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0

View File

@ -43,6 +43,7 @@ oslo.config.opts =
mixmatch = mixmatch.config:list_opts mixmatch = mixmatch.config:list_opts
mixmatch.extend = mixmatch.extend =
name_routing = mixmatch.extend.name_routing:NameRouting name_routing = mixmatch.extend.name_routing:NameRouting
networks_extended = mixmatch.extend.networks_extended:ExtendNetwork
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source