Browse Source

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
Jeremy Freudberg 1 year ago
parent
commit
955f28d397

+ 1
- 0
doc/source/index.rst View File

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

+ 223
- 0
doc/source/network-fed.rst View File

@@ -0,0 +1,223 @@
1
+==================
2
+Network Federation
3
+==================
4
+
5
+What is meant by "network federation"?
6
+======================================
7
+Mixmatch offers a mechanism to extend a network across clouds. Note that this
8
+idea of 'extending' is different than the direct access which the image
9
+federation and volume federation features offer. A user's choice to extend a
10
+network will usually be explicit and voluntary, whereas the sharing of images
11
+and volumes tends towards being implicit and automatic.
12
+
13
+Support for network federation requires that Neutron be backed by the ML2
14
+plugin. This plugin is often considered normal, or vanilla, so most clouds
15
+probably satisfy this requirement easily.
16
+
17
+The precise mechanism which allows the network federation feature to function
18
+is VXLAN tunneling between clouds.
19
+
20
+Finally, note that currently the scope of this feature is limited to extending
21
+networks from a remote cloud to the so-called 'local' cloud in which the
22
+Mixmatch proxy service resides.
23
+
24
+Network federation for operators
25
+================================
26
+Some steps must be taken to configure clouds in such a way that the network
27
+federation feature works as intended.
28
+
29
+Registering remote VXLAN endpoints
30
+----------------------------------
31
+In a single-cloud deployment, the Neutron ML2 plugin creates a VXLAN mesh
32
+among compute nodes, to allow virtual machines residing on separate physical
33
+hardware to communicate.
34
+
35
+The ability to manipulate the VXLAN mesh is not exposed by the Neutron API, so
36
+operators must edit database entries manually. Below, we use MySQL as an
37
+example, but operators should take care to translate these queries to be
38
+compatibile with their own database.
39
+
40
+Below 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
+
55
+These entries are automatically populated by Neutron and contain references to
56
+each compute node in the cloud.
57
+
58
+In order to allow networks to extend across clouds, operators should simply
59
+insert 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
+
78
+Finally, operators should take care to ensure that the incoming UDP traffic on
79
+port 4789 is in-fact permitted.
80
+
81
+**NOTE**: Similar steps to those above must be performed on each cloud, in
82
+order to support bidirectional traffic.
83
+
84
+Because managing numerous entries in the database can become unwieldy, an
85
+operator might consider installing some device, of an unknown nature, which
86
+could perform VXLAN termination for an entire cloud. A reference to this
87
+device would appear in the database instead of entries for each compute node.
88
+
89
+Configuring Neutron policies
90
+----------------------------
91
+The operations which Mixmatch performs to extend a network are, by default and
92
+by nature, privileged operations. The default Neutron policy restricts the
93
+performance of these operations to users with the ``admin`` role. Therefore, in
94
+its home cloud only the Mixmatch service user should have this role.
95
+
96
+In a federation of clouds, however, the landlord of each remote cloud will
97
+probably not want to give out this ``admin`` role. In fact he or she will want
98
+to only give the Mixmatch service user the minimal amount of elevated
99
+permissions needed to perform the network-extending operations, and no more.
100
+
101
+Therefore a new role, which we will call ``mixmatch_fancy_role``, should be
102
+created in each remote cloud. Operators should ensure that the Mixmatch service
103
+user is given this role in its mapped projects in those remote clouds. Then,
104
+the following entries in the Neutron ``policy.json`` file should be changed or
105
+added: (at the time of writing Neutron still does not have any default policies
106
+registered 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
+
118
+Note that due to limitations in Neutron's policy engine we must take advantage
119
+of the ``advsvc`` ("Advanced Services") permission feature, rather than define
120
+our own custom policy. Therefore, operators might want to additionally tweak
121
+the other default entries in policy.json which reference this role (mostly
122
+related to port operations).
123
+
124
+Ensuring non-conflicting VXLAN IDs
125
+----------------------------------
126
+Because Mixmatch will be creating new networks with a particular VXLAN ID
127
+specified, there may be conflicts if the various remote clouds assign these
128
+IDs randomly (the default behavior). In the
129
+``/etc/neutron/plugins/ml2/ml2_conf.ini`` file of each cloud, operators should
130
+take care to set a reasonable and non-overlapping ``start:end`` value for
131
+``[ml2_type_vxlan]/vni_ranges``.
132
+
133
+Network federation for users
134
+============================
135
+Users consume the network federation feature by sending requests to an
136
+extension of the Neutron API which is exposed by the Mixmatch proxy service.
137
+
138
+API reference
139
+-------------
140
+The details of that API call follow below. (Note that because the network
141
+extending is always performed as remote-to-local, the ``MM-SERVICE-PROVIDER``
142
+header 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
+
160
+On success, the response of this API call will be identical in format to the
161
+standard Neutron POST ``/v2.0/networks``. On failure, there are several
162
+specific 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
+
173
+Subnet management
174
+-----------------
175
+Note however, that it will remain the responsibility of the user to manage
176
+the subnets of extended networks. In other words, the network-extending
177
+functionality which Mixmatch exposes does not perform any subnet operations.
178
+
179
+Users should take care to make sure that for the subnet in each cloud, the
180
+first three octets of the (IPv4) subnet are the same, but that the allocation
181
+pools do not overlap. Additionally, the user should ensure that DHCP is only
182
+enabled for the subnet of one cloud and not the other. (The choice of which
183
+subnet will offer DHCP can, in practice, be an arbitrary one.) Users can have
184
+the two subnets share one router ("gateway"), or have a separate gateway for
185
+each cloud.
186
+
187
+Some 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
+..

+ 1
- 0
lower-constraints.txt View File

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

+ 20
- 6
mixmatch/auth.py View File

@@ -30,9 +30,9 @@ MEMOIZE_SESSION = config.auth.MEMOIZE
30 30
 
31 31
 
32 32
 @MEMOIZE_SESSION
33
-def get_client():
34
-    """Return a Keystone client capable of validating tokens."""
35
-    LOG.info("Getting Admin Client")
33
+def get_admin_session(sp=None):
34
+    """Return a Keystone session using admin service credentials."""
35
+    LOG.info("Getting Admin Session")
36 36
     service_auth = identity.Password(
37 37
         auth_url=CONF.auth.auth_url,
38 38
         username=CONF.auth.username,
@@ -41,15 +41,29 @@ def get_client():
41 41
         project_domain_id=CONF.auth.project_domain_id,
42 42
         user_domain_id=CONF.auth.user_domain_id
43 43
     )
44
-    local_session = session.Session(auth=service_auth)
45
-    return v3.client.Client(session=local_session)
44
+    sess = session.Session(auth=service_auth)
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
55
+def 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 61
 @MEMOIZE_SESSION
49 62
 def get_local_auth(user_token):
50 63
     """Return a Keystone session for the local cluster."""
51 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 67
     token = v3.tokens.TokenManager(client)
54 68
 
55 69
     try:

+ 9
- 0
mixmatch/extend/base.py View File

@@ -37,3 +37,12 @@ class Extension(object):
37 37
 
38 38
     def handle_response(self, response):
39 39
         pass
40
+
41
+
42
+class 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

+ 103
- 0
mixmatch/extend/networks_extended.py View File

@@ -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
+
15
+from mixmatch import auth
16
+from mixmatch.config import CONF
17
+from mixmatch.extend import base
18
+from mixmatch import utils
19
+
20
+import flask
21
+from neutronclient.v2_0 import client as neutron
22
+from neutronclient.common import exceptions as n_ex
23
+from oslo_serialization import jsonutils
24
+
25
+
26
+class 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
+        )

+ 6
- 1
mixmatch/proxy.py View File

@@ -18,6 +18,7 @@ import requests
18 18
 from urllib3.util import retry
19 19
 import flask
20 20
 from flask import abort
21
+import functools
21 22
 
22 23
 from mixmatch import config
23 24
 from mixmatch.config import LOG, CONF, service_providers
@@ -117,8 +118,12 @@ class RequestHandler(object):
117 118
 
118 119
         self.append_proxy(self.details.headers)
119 120
 
121
+        # TODO(jfreud): more sophisticated/ordered invocation of extensions
120 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 128
         if not self.details.version:
124 129
             if CONF.aggregation:

+ 1
- 0
requirements.txt View File

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

+ 1
- 0
setup.cfg View File

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

Loading…
Cancel
Save