diff --git a/doc/source/index.rst b/doc/source/index.rst index f7c9188..3cab81e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -19,6 +19,7 @@ Operators Guide installation identity volumes + network-fed * `Release Notes `_ diff --git a/doc/source/network-fed.rst b/doc/source/network-fed.rst new file mode 100644 index 0000000..70707f1 --- /dev/null +++ b/doc/source/network-fed.rst @@ -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 /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}) +.. diff --git a/lower-constraints.txt b/lower-constraints.txt index cd36505..34c3947 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -77,6 +77,7 @@ python-dateutil==2.7.0 python-editor==1.0.3 python-keystoneclient==3.8.0 python-mimeparse==1.6.0 +python-neutronclient==6.7.0 python-subunit==1.0.0 pytz==2018.3 PyYAML==3.12 diff --git a/mixmatch/auth.py b/mixmatch/auth.py index 4ec8284..ffb7ba7 100644 --- a/mixmatch/auth.py +++ b/mixmatch/auth.py @@ -30,9 +30,9 @@ MEMOIZE_SESSION = config.auth.MEMOIZE @MEMOIZE_SESSION -def get_client(): - """Return a Keystone client capable of validating tokens.""" - LOG.info("Getting Admin Client") +def get_admin_session(sp=None): + """Return a Keystone session using admin service credentials.""" + LOG.info("Getting Admin Session") service_auth = identity.Password( auth_url=CONF.auth.auth_url, username=CONF.auth.username, @@ -41,15 +41,29 @@ def get_client(): project_domain_id=CONF.auth.project_domain_id, user_domain_id=CONF.auth.user_domain_id ) - local_session = session.Session(auth=service_auth) - return v3.client.Client(session=local_session) + sess = session.Session(auth=service_auth) + 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 def get_local_auth(user_token): """Return a Keystone session for the local cluster.""" 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) try: diff --git a/mixmatch/extend/base.py b/mixmatch/extend/base.py index 5953c52..c92a3fb 100644 --- a/mixmatch/extend/base.py +++ b/mixmatch/extend/base.py @@ -37,3 +37,12 @@ class Extension(object): def handle_response(self, response): pass + + +class FinalResponse(object): + stream = False + + def __init__(self, text, status_code, headers): + self.text = text + self.status_code = status_code + self.headers = headers diff --git a/mixmatch/extend/networks_extended.py b/mixmatch/extend/networks_extended.py new file mode 100644 index 0000000..782cf1f --- /dev/null +++ b/mixmatch/extend/networks_extended.py @@ -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'} + ) diff --git a/mixmatch/proxy.py b/mixmatch/proxy.py index c8a9e51..d6592db 100644 --- a/mixmatch/proxy.py +++ b/mixmatch/proxy.py @@ -18,6 +18,7 @@ import requests from urllib3.util import retry import flask from flask import abort +import functools from mixmatch import config from mixmatch.config import LOG, CONF, service_providers @@ -117,8 +118,12 @@ class RequestHandler(object): self.append_proxy(self.details.headers) + # TODO(jfreud): more sophisticated/ordered invocation of 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 CONF.aggregation: diff --git a/requirements.txt b/requirements.txt index 1f2f6e0..3e4d22d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ oslo.db>=4.27.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 keystoneauth1>=3.4.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 six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 4b52ce1..c9750bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ oslo.config.opts = mixmatch = mixmatch.config:list_opts mixmatch.extend = name_routing = mixmatch.extend.name_routing:NameRouting + networks_extended = mixmatch.extend.networks_extended:ExtendNetwork [build_sphinx] source-dir = doc/source