summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Freudberg <jeremyfreudberg@gmail.com>2017-10-06 23:12:31 +0000
committerJeremy Freudberg <jeremyfreudberg@gmail.com>2018-05-15 18:19:26 +0000
commit955f28d3973b442b93a7249b0de8570aaf7801f1 (patch)
treea99f7e11854472adf151a74c788b7c93ad3f3239
parent59af61ff6727345a1acc046b2490f5008d07448c (diff)
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
Notes
Notes (review): Code-Review+2: Kristi Nikolla <knikolla@bu.edu> Workflow+1: Kristi Nikolla <knikolla@bu.edu> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Tue, 15 May 2018 20:02:25 +0000 Reviewed-on: https://review.openstack.org/510245 Project: openstack/mixmatch Branch: refs/heads/master
-rw-r--r--doc/source/index.rst1
-rw-r--r--doc/source/network-fed.rst223
-rw-r--r--lower-constraints.txt1
-rw-r--r--mixmatch/auth.py26
-rw-r--r--mixmatch/extend/base.py9
-rw-r--r--mixmatch/extend/networks_extended.py103
-rw-r--r--mixmatch/proxy.py7
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg1
9 files changed, 365 insertions, 7 deletions
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
19 installation 19 installation
20 identity 20 identity
21 volumes 21 volumes
22 network-fed
22 23
23* `Release Notes <https://mixmatch.readthedocs.org/projects/releasenotes>`_ 24* `Release Notes <https://mixmatch.readthedocs.org/projects/releasenotes>`_
24 25
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 @@
1==================
2Network Federation
3==================
4
5What is meant by "network federation"?
6======================================
7Mixmatch offers a mechanism to extend a network across clouds. Note that this
8idea of 'extending' is different than the direct access which the image
9federation and volume federation features offer. A user's choice to extend a
10network will usually be explicit and voluntary, whereas the sharing of images
11and volumes tends towards being implicit and automatic.
12
13Support for network federation requires that Neutron be backed by the ML2
14plugin. This plugin is often considered normal, or vanilla, so most clouds
15probably satisfy this requirement easily.
16
17The precise mechanism which allows the network federation feature to function
18is VXLAN tunneling between clouds.
19
20Finally, note that currently the scope of this feature is limited to extending
21networks from a remote cloud to the so-called 'local' cloud in which the
22Mixmatch proxy service resides.
23
24Network federation for operators
25================================
26Some steps must be taken to configure clouds in such a way that the network
27federation feature works as intended.
28
29Registering remote VXLAN endpoints
30----------------------------------
31In a single-cloud deployment, the Neutron ML2 plugin creates a VXLAN mesh
32among compute nodes, to allow virtual machines residing on separate physical
33hardware to communicate.
34
35The ability to manipulate the VXLAN mesh is not exposed by the Neutron API, so
36operators must edit database entries manually. Below, we use MySQL as an
37example, but operators should take care to translate these queries to be
38compatibile with their own database.
39
40Below is how the database entries may appear for a single-cloud deployment:
41
42.. sourcecode:: console
43
44 mysql> select * from neutron.ml2_vxlan_endpoints;
45 +-------------+----------+------------+
46 | ip_address | udp_port | host |
47 +-------------+----------+------------+
48 | 10.19.97.20 | 4789 | compute-01 |
49 | 10.19.97.21 | 4789 | controller |
50 | 10.19.97.22 | 4789 | compute-02 |
51 +-------------+----------+------------+
52 3 rows in set (0.00 sec)
53..
54
55These entries are automatically populated by Neutron and contain references to
56each compute node in the cloud.
57
58In order to allow networks to extend across clouds, operators should simply
59insert entries for the compute nodes in remote clouds:
60
61.. sourcecode:: console
62
63 mysql> insert into neutron.ml2_vxlan_endpoints (ip_address, udp_port, host) values ('129.10.5.10', 4789, 'compute-01.remotecloud.org');
64 Query OK, 1 row affected (0.00 sec)
65
66 mysql> select * from neutron.ml2_vxlan_endpoints;
67 +-------------+----------+----------------------------+
68 | ip_address | udp_port | host |
69 +-------------+----------+----------------------------+
70 | 10.19.97.20 | 4789 | compute-01 |
71 | 10.19.97.21 | 4789 | controller |
72 | 10.19.97.22 | 4789 | compute-02 |
73 | 129.10.5.10 | 4789 | compute-01.remotecloud.org |
74 +-------------+----------+----------------------------+
75 4 rows in set (0.00 sec)
76..
77
78Finally, operators should take care to ensure that the incoming UDP traffic on
79port 4789 is in-fact permitted.
80
81**NOTE**: Similar steps to those above must be performed on each cloud, in
82order to support bidirectional traffic.
83
84Because managing numerous entries in the database can become unwieldy, an
85operator might consider installing some device, of an unknown nature, which
86could perform VXLAN termination for an entire cloud. A reference to this
87device would appear in the database instead of entries for each compute node.
88
89Configuring Neutron policies
90----------------------------
91The operations which Mixmatch performs to extend a network are, by default and
92by nature, privileged operations. The default Neutron policy restricts the
93performance of these operations to users with the ``admin`` role. Therefore, in
94its home cloud only the Mixmatch service user should have this role.
95
96In a federation of clouds, however, the landlord of each remote cloud will
97probably not want to give out this ``admin`` role. In fact he or she will want
98to only give the Mixmatch service user the minimal amount of elevated
99permissions needed to perform the network-extending operations, and no more.
100
101Therefore a new role, which we will call ``mixmatch_fancy_role``, should be
102created in each remote cloud. Operators should ensure that the Mixmatch service
103user is given this role in its mapped projects in those remote clouds. Then,
104the following entries in the Neutron ``policy.json`` file should be changed or
105added: (at the time of writing Neutron still does not have any default policies
106registered in code, so the rest of the policy file must stay intact)
107
108.. sourcecode:: json
109
110 {
111 "mixmatch": "role:mixmatch_fancy_role",
112 "context_is_advsvc": "rule:mixmatch",
113
114 "get_network:provider:segmentation_id": "rule:admin_only or rule:mixmatch"
115 }
116..
117
118Note that due to limitations in Neutron's policy engine we must take advantage
119of the ``advsvc`` ("Advanced Services") permission feature, rather than define
120our own custom policy. Therefore, operators might want to additionally tweak
121the other default entries in policy.json which reference this role (mostly
122related to port operations).
123
124Ensuring non-conflicting VXLAN IDs
125----------------------------------
126Because Mixmatch will be creating new networks with a particular VXLAN ID
127specified, there may be conflicts if the various remote clouds assign these
128IDs randomly (the default behavior). In the
129``/etc/neutron/plugins/ml2/ml2_conf.ini`` file of each cloud, operators should
130take care to set a reasonable and non-overlapping ``start:end`` value for
131``[ml2_type_vxlan]/vni_ranges``.
132
133Network federation for users
134============================
135Users consume the network federation feature by sending requests to an
136extension of the Neutron API which is exposed by the Mixmatch proxy service.
137
138API reference
139-------------
140The details of that API call follow below. (Note that because the network
141extending is always performed as remote-to-local, the ``MM-SERVICE-PROVIDER``
142header is not understood by this call.)
143
144.. sourcecode:: console
145
146 POST <mixmatch url>/network/v2.0/networks/extended
147..
148
149.. sourcecode:: json
150
151 {
152 "network": {
153 "existing_net_id": "60ed86b2-8db8-4459-8d31-475345534dec",
154 "existing_net_sp": "some_remote_sp",
155 "name": "my_cool_extended_network"
156 }
157 }
158..
159
160On success, the response of this API call will be identical in format to the
161standard Neutron POST ``/v2.0/networks``. On failure, there are several
162specific error codes which can be returned:
163
164* 400, if ``existing_net_id`` or ``existing_net_sp`` are not present in the
165 request body
166* 401, if the user is unauthorized (no token or invalid token)
167* 409, if there is a naming conflict for the extended network
168* 422, if a request to Neutron ended with a client-side error (usually network
169 not found or not available to the user), or if the service provider is not
170 known to Mixmatch
171* 503, if a request to Neutron ended with a server-side error
172
173Subnet management
174-----------------
175Note however, that it will remain the responsibility of the user to manage
176the subnets of extended networks. In other words, the network-extending
177functionality which Mixmatch exposes does not perform any subnet operations.
178
179Users should take care to make sure that for the subnet in each cloud, the
180first three octets of the (IPv4) subnet are the same, but that the allocation
181pools do not overlap. Additionally, the user should ensure that DHCP is only
182enabled for the subnet of one cloud and not the other. (The choice of which
183subnet will offer DHCP can, in practice, be an arbitrary one.) Users can have
184the two subnets share one router ("gateway"), or have a separate gateway for
185each cloud.
186
187Some example code which may help in following these guidelines is found below:
188
189.. sourcecode:: console
190
191 old_subnet = (
192 [s for s in CLOUD1_NEUTRON_CLIENT.list_subnets()['subnets']
193 if (s['ip_version'] == 4 and
194 s['network_id'] == CLOUD1_NETWORK_ID)][0]
195 )
196 old_subnet_id = old_subnet['id']
197 old_subnet_start = old_subnet['allocation_pools'][0]['start']
198 maximum_ip = int(
199 old_subnet['allocation_pools'][0]['end']
200 .split('.')[-1]
201 )
202 pool_base = re.sub(r'\d+$', '', old_subnet_start)
203 CLOUD1_NEUTRON_CLIENT.update_subnet(
204 old_subnet_id, body={'subnet': {'allocation_pools':
205 [{'start': old_subnet_start,
206 'end': '{}{}'.format(
207 pool_base, maximum_ip // 2)}]}}
208 )
209 new_subnet_body = (
210 {'enable_dhcp': False,
211 'network_id': CLOUD2_NETWORK_ID,
212 'dns_nameservers': old_subnet['dns_nameservers'],
213 'ip_version': 4,
214 'gateway_ip': old_subnet['gateway_ip'],
215 'cidr': old_subnet['cidr'],
216 'allocation_pools':
217 [{'start': '{}{}'.format(pool_base, maximum_ip // 2 + 1),
218 'end': '{}{}'.format(pool_base, maximum_ip)}]
219 }
220 )
221 new_subnet = CLOUD2_NEUTRON_CLIENT.create_subnet(
222 body={'subnet': new_subnet_body})
223..
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
77python-editor==1.0.3 77python-editor==1.0.3
78python-keystoneclient==3.8.0 78python-keystoneclient==3.8.0
79python-mimeparse==1.6.0 79python-mimeparse==1.6.0
80python-neutronclient==6.7.0
80python-subunit==1.0.0 81python-subunit==1.0.0
81pytz==2018.3 82pytz==2018.3
82PyYAML==3.12 83PyYAML==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
30 30
31 31
32@MEMOIZE_SESSION 32@MEMOIZE_SESSION
33def get_client(): 33def get_admin_session(sp=None):
34 """Return a Keystone client capable of validating tokens.""" 34 """Return a Keystone session using admin service credentials."""
35 LOG.info("Getting Admin Client") 35 LOG.info("Getting Admin Session")
36 service_auth = identity.Password( 36 service_auth = identity.Password(
37 auth_url=CONF.auth.auth_url, 37 auth_url=CONF.auth.auth_url,
38 username=CONF.auth.username, 38 username=CONF.auth.username,
@@ -41,15 +41,29 @@ def get_client():
41 project_domain_id=CONF.auth.project_domain_id, 41 project_domain_id=CONF.auth.project_domain_id,
42 user_domain_id=CONF.auth.user_domain_id 42 user_domain_id=CONF.auth.user_domain_id
43 ) 43 )
44 local_session = session.Session(auth=service_auth) 44 sess = session.Session(auth=service_auth)
45 return v3.client.Client(session=local_session) 45 if sp is None:
46 return sess
47 else:
48 token = sess.get_token()
49 project_id = get_projects_at_sp(sp, token)[0]
50 remote_admin_sess = get_sp_auth(sp, token, project_id)
51 return remote_admin_sess
52
53
54@MEMOIZE_SESSION
55def get_client(session):
56 """Return a client object given a session object."""
57 LOG.debug("Getting client for %s" % session)
58 return v3.client.Client(session=session)
46 59
47 60
48@MEMOIZE_SESSION 61@MEMOIZE_SESSION
49def get_local_auth(user_token): 62def get_local_auth(user_token):
50 """Return a Keystone session for the local cluster.""" 63 """Return a Keystone session for the local cluster."""
51 LOG.debug("Getting session for %s" % user_token) 64 LOG.debug("Getting session for %s" % user_token)
52 client = get_client() 65 admin_session = get_admin_session()
66 client = get_client(admin_session)
53 token = v3.tokens.TokenManager(client) 67 token = v3.tokens.TokenManager(client)
54 68
55 try: 69 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):
37 37
38 def handle_response(self, response): 38 def handle_response(self, response):
39 pass 39 pass
40
41
42class FinalResponse(object):
43 stream = False
44
45 def __init__(self, text, status_code, headers):
46 self.text = text
47 self.status_code = status_code
48 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 @@
1# Copyright 2017 Massachusetts Open Cloud
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from mixmatch import auth
16from mixmatch.config import CONF
17from mixmatch.extend import base
18from mixmatch import utils
19
20import flask
21from neutronclient.v2_0 import client as neutron
22from neutronclient.common import exceptions as n_ex
23from oslo_serialization import jsonutils
24
25
26class ExtendNetwork(base.Extension):
27 """An extension which smells like Neutron's POST /networks.
28
29 It extends networks by matching up VXLAN IDs.
30 """
31
32 ROUTES = [
33 ('/network/v2.0/networks/extended', ['POST']),
34 # For now, mask Neutron POST /networks. Later, move the extend-network
35 # logic into a new, separate API.
36 ]
37
38 @staticmethod
39 def _has_access(net_id, remote_project_ids, origin_sp, user_tok):
40 for remote_project_id in remote_project_ids:
41 sp_sess = auth.get_sp_auth(origin_sp, user_tok, remote_project_id)
42 remote_user_client = neutron.Client(session=sp_sess)
43 try:
44 remote_user_client.show_network(net_id)
45 return True
46 except n_ex.NeutronClientException as e:
47 if e.status_code < 500:
48 continue
49 else:
50 flask.abort(503)
51 return False
52
53 def handle_request(self, request):
54 body = jsonutils.loads(request.body)
55
56 origin_sp = utils.safe_pop(body['network'], 'existing_net_sp')
57 existing_net_id = utils.safe_pop(body['network'], 'existing_net_id')
58 user_tok = request.token
59
60 if origin_sp is None or existing_net_id is None:
61 flask.abort(400)
62 if origin_sp not in CONF.service_providers:
63 flask.abort(422)
64
65 remote_admin_sess = auth.get_admin_session(origin_sp)
66 remote_admin_neutronclient = neutron.Client(session=remote_admin_sess)
67
68 try:
69 original = (
70 remote_admin_neutronclient.show_network(existing_net_id)
71 )
72 except n_ex.NeutronClientException as e:
73 flask.abort(422 if e.status_code < 500 else 503)
74
75 remote_project_ids = auth.get_projects_at_sp(origin_sp, user_tok)
76 if not self._has_access(existing_net_id, remote_project_ids,
77 origin_sp, user_tok):
78 flask.abort(422)
79
80 local_admin_session = auth.get_admin_session()
81 local_admin_neutronclient = (
82 neutron.Client(session=local_admin_session)
83 )
84
85 body['network']['provider:network_type'] = 'vxlan'
86 vxlan_id = original['network']['provider:segmentation_id']
87 body['network']['provider:segmentation_id'] = vxlan_id
88 local_project_id = auth.get_local_auth(user_tok).get_project_id()
89 body['network']['project_id'] = local_project_id
90
91 try:
92 new_net = local_admin_neutronclient.create_network(body)
93 except n_ex.Conflict:
94 # Conflict could happen when names collide. So, give client error.
95 flask.abort(409)
96 except n_ex.NeutronClientException:
97 flask.abort(503)
98
99 return base.FinalResponse(
100 jsonutils.dumps(new_net),
101 201,
102 headers={'Content-Type': 'application/json'}
103 )
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
18from urllib3.util import retry 18from urllib3.util import retry
19import flask 19import flask
20from flask import abort 20from flask import abort
21import functools
21 22
22from mixmatch import config 23from mixmatch import config
23from mixmatch.config import LOG, CONF, service_providers 24from mixmatch.config import LOG, CONF, service_providers
@@ -117,8 +118,12 @@ class RequestHandler(object):
117 118
118 self.append_proxy(self.details.headers) 119 self.append_proxy(self.details.headers)
119 120
121 # TODO(jfreud): more sophisticated/ordered invocation of extensions
120 for extension in self.extensions: 122 for extension in self.extensions:
121 extension.handle_request(self.details) 123 out = extension.handle_request(self.details)
124 if out is not None:
125 self._forward = functools.partial(self._finalize, out)
126 return
122 127
123 if not self.details.version: 128 if not self.details.version:
124 if CONF.aggregation: 129 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
15oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 15oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
16keystoneauth1>=3.4.0 # Apache-2.0 16keystoneauth1>=3.4.0 # Apache-2.0
17python-keystoneclient>=3.8.0 # Apache-2.0 17python-keystoneclient>=3.8.0 # Apache-2.0
18python-neutronclient>=6.7.0 # Apache-2.0
18requests>=2.14.2 # Apache-2.0 19requests>=2.14.2 # Apache-2.0
19six>=1.10.0 # MIT 20six>=1.10.0 # MIT
20stevedore>=1.20.0 # Apache-2.0 21stevedore>=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 =
43 mixmatch = mixmatch.config:list_opts 43 mixmatch = mixmatch.config:list_opts
44mixmatch.extend = 44mixmatch.extend =
45 name_routing = mixmatch.extend.name_routing:NameRouting 45 name_routing = mixmatch.extend.name_routing:NameRouting
46 networks_extended = mixmatch.extend.networks_extended:ExtendNetwork
46 47
47[build_sphinx] 48[build_sphinx]
48source-dir = doc/source 49source-dir = doc/source