Browse Source

Add ML2 mechanism driver for FUJITSU C-Fabric

This patch provides the ML2 mechanism driver for FUJITSU Converged Fabric
Switch(C-Fabric).[1]

[1] https://bugs.launchpad.net/neutron/+bug/1464194

Change-Id: Ib4189c139c1154596779ff807287900294119f50
tags/1.0.0
Yushiro FURUKAWA 3 years ago
parent
commit
761cd40e5d

+ 156
- 7
README.rst View File

@@ -2,18 +2,167 @@
2 2
 networking-fujitsu
3 3
 ===============================
4 4
 
5
-Fujitsu ML2 plugins/drivers for OpenStack Neutron
5
+FUJITSU plugins/drivers for OpenStack Neutron.
6
+Following mechanism drivers are available in this repository:
6 7
 
7
-Please feel here a long description which must be at least 3 lines wrapped on
8
-80 cols, so that distribution package maintainers can use it in their packages.
9
-Note that this is a hard requirement.
8
+1. (ML2) Mechanism driver for FUJITSU Converged Fabric Switch(C-Fabric)
9
+2. ...
10 10
 
11 11
 * Free software: Apache license
12 12
 * Documentation: http://docs.openstack.org/developer/networking-fujitsu
13 13
 * Source: http://git.openstack.org/cgit/openstack/networking-fujitsu
14 14
 * Bugs: http://bugs.launchpad.net/networking-fujitsu
15 15
 
16
-Features
17
---------
16
+Mechanism driver for FUJITSU Converged Fabric Switch(C-Fabric)
17
+==============================================================
18 18
 
19
-* TODO
19
+How to Install
20
+--------------
21
+
22
+1. Install the package::
23
+
24
+    $ pip install networking-fujitsu
25
+
26
+2. Add ``fujitsu`` to mechanism_drivers option in
27
+   /etc/neutron/plugins/ml2/ml2_conf.ini, for example::
28
+
29
+     mechanism_drivers = openvswitch,fujitsu_cfab
30
+
31
+3. Modify ml2_conf_fujitsu.ini and make neutron-server to read it.
32
+
33
+   For RedHat, add the following options in ExecStart in
34
+   /usr/lib/systemd/system/neutron-server.service::
35
+
36
+     --config-file /etc/neutron/plugins/ml2/ml2_conf_fujitsu.ini
37
+
38
+   For Ubuntu, add the following line to /etc/default/neutron-server::
39
+
40
+     NEUTRON_PLUGIN_ML2_CONFIG="/etc/neutron/plugins/ml2/ml2_conf_fujitsu.ini"
41
+
42
+   and add the following line before 'exec start-stop-daemon ...' in
43
+   /etc/init/neutron-server.conf::
44
+
45
+     [ -r "$NEUTRON_PLUGIN_ML2_CONFIG" ] && CONF_ARG="${CONF_ARG} --config-file $NEUTRON_PLUGIN_ML2_CONFIG"
46
+
47
+Configuration
48
+-------------
49
+
50
+Only VLAN network type is supported (ie. both ``type_drivers`` and
51
+``tenant_network_types`` in ``[ml2]`` section of configuration files
52
+should be ``vlan``).
53
+
54
+The following parameters can be specified in ``[fujitsu_cfab]``
55
+section of configuration files (such as ml2_conf_fujitsu.ini).
56
+
57
+``address``
58
+  The IP address or the host name of the C-Fabric to connect to using
59
+  telnet protocol. This is a mandatory parameter and it has no
60
+  default value. Only one address can be specified.
61
+
62
+  Example::
63
+
64
+    address = 192.168.0.1
65
+
66
+``username``
67
+  The C-Fabric username to use. Please note that the user must have
68
+  administrator rights to configure C-Fabric. The default value is
69
+  ``admin``.
70
+
71
+  Example::
72
+
73
+    username = admin
74
+
75
+``password``
76
+  The C-Fabric password to use. The default value is ``admin``.
77
+
78
+  Example::
79
+
80
+    password = admin
81
+
82
+``physical_networks``
83
+  List of <physical_network>:<vfab_id> tuples specifying physical
84
+  network names and corresponding vfab IDs. All possible physical
85
+  network names must be specified in this parameter. If a physical
86
+  network name not specified in this parameter is used, a runtime
87
+  exception will be raised. It is valid to use same vfab ID for
88
+  different physical networks as long as VLAN IDs are exclusive.
89
+  Please note that vfabs must be created and configured in C-Fabric
90
+  beforehand.
91
+
92
+  Example::
93
+
94
+    physical_networks = physnet1:1,physnet2:2
95
+
96
+``share_pprofile``
97
+  Whether to share a C-Fabric pprofile among Neutron ports using the same VLAN
98
+  ID. If it is true, the pprofile name will be based on the VLAN ID, and the
99
+  pprofile will be used for all Neutron ports using the same VLAN ID. If it is
100
+  false, the pprofile name will be based on the MAC address, and each Neutron
101
+  port will use dedicated pprofile. The default value is ``False``.
102
+
103
+  Example::
104
+
105
+    share_pprofile = True
106
+
107
+``pprofile_prefix``
108
+  The prefix string for pprofile names. The pprofile name will be
109
+  "<pprofile_prefix> + <vlan_id>" or "<pprofile_prefix> + <MAC_address>"
110
+  according to the ``share_pprofile`` parameter. If ``pprofile_prefix`` is
111
+  specified, the mechanism driver will not use the existing pprofiles
112
+  which do not have the prefix. If ``pprofile_prefix`` is not specified, the
113
+  mechanism driver will use the existing pprofile if it corresponds to the VLAN
114
+  ID when ``share_pprofile`` is true, or if the name ends with the MAC address
115
+  when ``share_pprofile`` is false.
116
+
117
+  Example::
118
+
119
+    pprofile_prefix = neutron-
120
+
121
+``save_config``
122
+  Whether to save configuration. If it is true, C-Fabric's
123
+  configuration will be saved every time the configuration is
124
+  committed. The default value is ``True``.
125
+
126
+  Example::
127
+
128
+    save_config = False
129
+
130
+C-Fabric Configuration
131
+----------------------
132
+
133
+As well as the standard configuration of C-Fabric, the following
134
+configurations are needed for the mechanism driver.
135
+
136
+1. Enable AMPP using ARP/DHCP.
137
+
138
+   By default, only RARP packets are examined for AMPP. It is
139
+   possible to add ARP/DHCP packets to be examined for AMPP.
140
+
141
+   Example::
142
+
143
+     evb ampp arp on
144
+     evb ampp dhcp on
145
+
146
+   Please note that ``evb ampp dhcp`` is not supported in earlier
147
+   versions of C-Fabric firmware.  Therefore, please create the subnet
148
+   with enable_dhcp is FALSE before ampp dhcp function is supported.
149
+
150
+2. Create and configure vfabs.
151
+
152
+   It is necessary to create and configure the vfab beforehand. It is
153
+   recommended that the ports connected to the network nodes are
154
+   configured as VLAN through mode.
155
+
156
+   Example::
157
+
158
+     ifgroup 0 ether 1/1/0/1-1/1/0/18
159
+     ifgroup 1 ether 1/1/0/19-1/1/0/26
160
+     ifgroup 2 ether 1/2/0/1
161
+     vfab 1 cir-ports ifgroup 1
162
+     vfab 1 ampp-area 0
163
+     vfab 1 through ifgroup 2
164
+     interface 1/2/0/1
165
+         vfab through mode on
166
+
167
+   Please note that ``vfab through`` commands are only available on
168
+   C-Fabric firmware V02.30 and later.

+ 30
- 0
etc/neutron/plugins/ml2/ml2_conf_fujitsu_cfab.ini View File

@@ -0,0 +1,30 @@
1
+[fujitsu_cfab]
2
+# (StrOpt) The address of the C-Fabric to telnet to.
3
+# address =
4
+# Example: address = 192.168.0.1
5
+
6
+# (StrOpt) The C-Fabric username to use.
7
+# username = admin
8
+# Example: username = admin
9
+
10
+# (StrOpt) The C-Fabric password to use.
11
+# password = admin
12
+# Example: password = admin
13
+
14
+# (ListOpt) List of <physical_network>:<vfab_id> tuples specifying
15
+# physical_network names and corresponding vfab ids.
16
+# physical_networks =
17
+# Example: physical_networks = physnet1:1,physnet2:2
18
+
19
+# (BoolOpt) Whether to share a C-Fabric pprofile among Neutron ports using the
20
+# same VLAN ID.
21
+# share_pprofile = False
22
+# Example: share_pprofile = True
23
+
24
+# (StrOpt) The prefix string for pprofile name.
25
+# pprofile_prefix =
26
+# Example: pprofile_prefix = neutron-
27
+
28
+# (BoolOpt) Whether to save configuration.
29
+# save_config = True
30
+# Example: save_config = False

+ 1
- 1
networking_fujitsu/__init__.py View File

@@ -16,4 +16,4 @@ import pbr.version
16 16
 
17 17
 
18 18
 __version__ = pbr.version.VersionInfo(
19
-    'networking_fujitsu').version_string()
19
+    'networking_fujitsu').version_string()

+ 0
- 0
networking_fujitsu/ml2/__init__.py View File


+ 0
- 0
networking_fujitsu/ml2/drivers/__init__.py View File


+ 0
- 0
networking_fujitsu/ml2/drivers/fujitsu/__init__.py View File


+ 0
- 0
networking_fujitsu/ml2/drivers/fujitsu/cfab/__init__.py View File


+ 537
- 0
networking_fujitsu/ml2/drivers/fujitsu/cfab/cfabdriver.py View File

@@ -0,0 +1,537 @@
1
+# Copyright 2015 FUJITSU LIMITED
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
+
16
+
17
+"""Fujitsu C-Fabric Driver implements CLI over telnet for
18
+Neutron network life-cycle management.
19
+"""
20
+
21
+import re
22
+import select
23
+
24
+import eventlet
25
+telnetlib = eventlet.import_patched('telnetlib')
26
+
27
+try:
28
+    from oslo_config import cfg
29
+except ImportError:
30
+    from oslo.config import cfg
31
+try:
32
+    from oslo_log import log as logging
33
+except ImportError:
34
+    from neutron.openstack.common import log as logging
35
+try:
36
+    from oslo_utils import excutils
37
+except ImportError:
38
+    from neutron.openstack.common import excutils
39
+
40
+from neutron.common import utils
41
+try:
42
+    from neutron.i18n import _LE
43
+    from neutron.i18n import _LW
44
+except ImportError:
45
+    try:
46
+        from neutron.openstack.common._i18n import _LE
47
+        from neutron.openstack.common._i18n import _LW
48
+    except ImportError:
49
+        from neutron.openstack.common.gettextutils import _LE
50
+        from neutron.openstack.common.gettextutils import _LW
51
+from neutron.plugins.ml2.common import exceptions as ml2_exc
52
+
53
+
54
+LOG = logging.getLogger(__name__)
55
+TELNET_PORT = 23
56
+
57
+_LOCK_NAME = 'fujitsu'
58
+_TIMEOUT = 30
59
+_TIMEOUT_LOGIN = 5
60
+_CRLF_RE = re.compile(r"\r\n", re.MULTILINE)
61
+_PROMPT_LOGIN = "Login: "
62
+_PROMPT_PASS = "Password: "
63
+_PROMPT_ADMIN = "# "
64
+_PROMPT_CONFIG = "(config)# "
65
+_PROMPTS_RE = re.compile(
66
+    r"(((\(config(-if)?\))?#)|((?<!<ERROR)>)|((Login|Password):)) $")
67
+_ADMIN_PROMPTS_RE = re.compile(r"((\(config(-if)?\))?#) $")
68
+
69
+_MODE_LOGIN = 'login'
70
+_MODE_ADMIN = 'admin'
71
+_MODE_CONFIG = 'config'
72
+_MODE_CONFIG_IF = 'config-if'
73
+_MODE_USER = 'user'
74
+
75
+_PAGER_ENABLE_RE = re.compile(r"^pager\s+enable", re.MULTILINE)
76
+_INDEX_RE = re.compile(r"^(\d+)\s+", re.MULTILINE)
77
+_VFAB_PPROFILE_RE = re.compile(
78
+    r"^vfab\s+(default|\d+)\s+pprofile\s+(\d+)\s+"
79
+    r"vsiid\s+(?:mac|uuid)\s+\S+\s+(\S+)", re.MULTILINE)
80
+_PPROFILE_INDICES = frozenset(range(0, 4096))
81
+
82
+
83
+class _CFABManager(object):
84
+    """C-Fabric CLI manager.
85
+
86
+    Manages C-Fabric using a single telnet connection.
87
+    """
88
+
89
+    def __init__(self):
90
+        self._address = None
91
+        self._username = None
92
+        self._password = None
93
+        self._telnet = None
94
+        self._timeout = _TIMEOUT
95
+        self.save_config = True
96
+
97
+    def connect(self, address, username, password):
98
+        """Connect via TELNET and initialize the CLI session."""
99
+
100
+        # Use the persisted TELNET connection
101
+        if ((self._telnet and address == self._address and
102
+             username == self._username and password == self._password)):
103
+            return
104
+
105
+        # Connect using saved information
106
+        self._address = address
107
+        self._username = username
108
+        self._password = password
109
+        self._reconnect()
110
+
111
+    def get_running_config(self, prefix=None):
112
+        """Get running-config of the switch."""
113
+
114
+        terminal = self._execute("show terminal")
115
+        match = _PAGER_ENABLE_RE.search(terminal)
116
+        if match:
117
+            self._execute("terminal pager disable")
118
+        res = self._get_running_config_no_pager_control(prefix)
119
+        if match:
120
+            self._execute("terminal pager enable")
121
+        return res
122
+
123
+    def _get_running_config_no_pager_control(self, prefix=None):
124
+        """Get running-config of the switch without pager control."""
125
+
126
+        current = self._get_mode()
127
+        if current not in (_MODE_ADMIN, _MODE_CONFIG, _MODE_CONFIG_IF):
128
+            self._reconnect()
129
+        cmd = "show running-config"
130
+        if prefix:
131
+            cmd = " ".join([cmd, prefix])
132
+        return self._execute(cmd)
133
+
134
+    def configure(self, cmds, commit=True):
135
+        """Configures the switch."""
136
+
137
+        self._set_mode_config()
138
+        for cmd in cmds:
139
+            self._execute(cmd)
140
+        if commit:
141
+            self._execute("commit")
142
+            if self.save_config:
143
+                self._execute("save")
144
+
145
+    def _set_mode_config(self):
146
+        """Sets the configure mode of the switch."""
147
+
148
+        current = self._get_mode()
149
+        if current == _MODE_CONFIG:
150
+            return
151
+        if current == _MODE_ADMIN:
152
+            self._write("configure\n")
153
+            prompt = self._read_until(_PROMPT_CONFIG)
154
+        elif current == _MODE_CONFIG_IF:
155
+            self._write("exit\n")
156
+            prompt = self._read_until(_PROMPT_CONFIG)
157
+        else:
158
+            self._reconnect()
159
+            self._write("configure\n")
160
+            prompt = self._read_until(_PROMPT_CONFIG)
161
+        if prompt.find(_PROMPT_CONFIG) < 0:
162
+            LOG.error(_LE("Failed to set configure mode."))
163
+            raise ml2_exc.MechanismDriverError(method="_set_mode_config")
164
+
165
+    def _get_mode(self):
166
+        """Gets the current mode of the switch."""
167
+
168
+        self._read_eager()
169
+        self._write("\n")
170
+        idx, match, s = self._expect([_PROMPTS_RE])
171
+        if idx < 0 or match is None:
172
+            LOG.error(_LE("Unexpected response from switch: %s"), s)
173
+            raise ml2_exc.MechanismDriverError(method="_get_mode")
174
+        return _get_mode_from_match(match)
175
+
176
+    def _execute(self, cmd):
177
+        """Execute the command on the switch."""
178
+
179
+        self._write(cmd + "\n")
180
+        idx, match, s = self._expect([_PROMPTS_RE])
181
+        if idx < 0 or match is None:
182
+            LOG.error(_LE("Unexpected response from switch: %s"), s)
183
+            raise ml2_exc.MechanismDriverError(method="_execute")
184
+        if s.find("<ERROR>") >= 0:
185
+            LOG.error(_LE("Error is returned from switch: %s"), s)
186
+            raise ml2_exc.MechanismDriverError(method="_execute")
187
+        s = _CRLF_RE.sub(r"\n", s)
188
+        # Remove command and prompt
189
+        return s[s.find("\n") + 1:s.rfind("\n") + 1]
190
+
191
+    def _reconnect(self):
192
+        """Re-connect and initialize the CLI session."""
193
+
194
+        # Close the old connection
195
+        self._close_session()
196
+
197
+        # Open new TELNET connection
198
+        try:
199
+            self._telnet = telnetlib.Telnet(
200
+                host=self._address, port=TELNET_PORT, timeout=self._timeout)
201
+        except EnvironmentError:
202
+            with excutils.save_and_reraise_exception():
203
+                LOG.exception(_LE("Connect failed to switch"))
204
+        try:
205
+            prompt = self._telnet.read_until(_PROMPT_LOGIN, _TIMEOUT_LOGIN)
206
+            prompt.index(_PROMPT_LOGIN)
207
+            self._telnet.write(self._username + "\n")
208
+            prompt = self._telnet.read_until(_PROMPT_PASS, _TIMEOUT_LOGIN)
209
+            prompt.index(_PROMPT_PASS)
210
+            self._telnet.write(self._password + "\n")
211
+            prompt = self._telnet.read_until(_PROMPT_ADMIN, _TIMEOUT_LOGIN)
212
+            prompt.index(_PROMPT_ADMIN)
213
+        except (EOFError, EnvironmentError, ValueError):
214
+            self._telnet.close()
215
+            self._telnet = None
216
+            with excutils.save_and_reraise_exception():
217
+                LOG.exception(_LE("Login failed to switch"))
218
+
219
+        LOG.debug("Connect success to address %(address)s:%(telnet_port)d",
220
+                  dict(address=self._address, telnet_port=TELNET_PORT))
221
+
222
+    def _close_session(self):
223
+        """Close TELNET session."""
224
+
225
+        if self._telnet:
226
+            self._telnet.close()
227
+            self._telnet = None
228
+
229
+    def _write(self, buffer):
230
+        """Write a string to the switch."""
231
+
232
+        if not self._telnet and self._address:
233
+            self.connect(self._address, self._username, self._password)
234
+        try:
235
+            self._telnet.write(buffer)
236
+        except EnvironmentError:
237
+            self._close_session()
238
+            try:
239
+                self._reconnect()
240
+                self._telnet.write(buffer)
241
+            except EnvironmentError:
242
+                with excutils.save_and_reraise_exception():
243
+                    LOG.exception(_LE("Write failed to switch"))
244
+
245
+    def _read_eager(self):
246
+        """Read readily available data."""
247
+        if not self._telnet and self._address:
248
+            self.connect(self._address, self._username, self._password)
249
+        try:
250
+            return self._telnet.read_eager()
251
+        except (EOFError, EnvironmentError):
252
+            self._close_session()
253
+            try:
254
+                self._reconnect()
255
+                return self._telnet.read_eager()
256
+            except (EOFError, EnvironmentError):
257
+                with excutils.save_and_reraise_exception():
258
+                    LOG.exception(_LE("Read failed from switch"))
259
+
260
+    def _read_until(self, match):
261
+        """Read until a given string is encountered or until timeout."""
262
+
263
+        if not self._telnet and self._address:
264
+            self.connect(self._address, self._username, self._password)
265
+        try:
266
+            return self._telnet.read_until(match, self._timeout)
267
+        except (EOFError, EnvironmentError):
268
+            self._close_session()
269
+            try:
270
+                self._reconnect()
271
+                return self._telnet.read_until(match, self._timeout)
272
+            except (EOFError, EnvironmentError):
273
+                with excutils.save_and_reraise_exception():
274
+                    LOG.exception(_LE("Read failed from switch"))
275
+
276
+    def _expect(self, res):
277
+        """Read until one from a list of a regular expressions matches."""
278
+
279
+        if not self._telnet and self._address:
280
+            self.connect(self._address, self._username, self._password)
281
+        try:
282
+            return self._telnet.expect(res, timeout=self._timeout)
283
+        except (EOFError, EnvironmentError, select.error):
284
+            self._close_session()
285
+            try:
286
+                self._reconnect()
287
+                return self._telnet.expect(res, timeout=self._timeout)
288
+            except (EOFError, EnvironmentError, select.error):
289
+                with excutils.save_and_reraise_exception():
290
+                    LOG.exception(_LE("Read failed from switch"))
291
+
292
+
293
+def _get_mode_from_match(match):
294
+    """Determines the mode from the match object."""
295
+
296
+    mode = None
297
+    if match.group(4):
298
+        mode = _MODE_CONFIG_IF
299
+    elif match.group(3):
300
+        mode = _MODE_CONFIG
301
+    elif match.group(2):
302
+        mode = _MODE_ADMIN
303
+    elif match.group(5):
304
+        mode = _MODE_USER
305
+    elif match.group(6):
306
+        mode = _MODE_LOGIN
307
+    return mode
308
+
309
+CFAB_MANAGER = _CFABManager()
310
+
311
+
312
+class CFABdriver(object):
313
+    """C-Fabric CLI interface driver for Neutron network.
314
+
315
+    Handles life-cycle management of Neutron network (leverages AMPP on C-Fab)
316
+    """
317
+
318
+    def __init__(self, conf=None):
319
+        if conf:
320
+            self._conf = conf
321
+        else:
322
+            self._conf = cfg.CONF
323
+        self._share_pprofile = self._conf.fujitsu_cfab.share_pprofile
324
+        self._pprofile_prefix = self._conf.fujitsu_cfab.pprofile_prefix
325
+        self._validate_pprofile_prefix()
326
+        self._pprofile_name = "{prefix}{pid}"
327
+        self._save_config = self._conf.fujitsu_cfab.save_config
328
+        self.mgr = CFAB_MANAGER
329
+        self.mgr.save_config = self._save_config
330
+
331
+    def _validate_pprofile_prefix(self):
332
+        if len(self._pprofile_prefix) > (28 if self._share_pprofile else 15):
333
+            raise ValueError(_("pprofile_prefix is too long."))
334
+        if (self._pprofile_prefix.find('"') >= 0 or
335
+                self._pprofile_prefix.find("|") >= 0 or
336
+                self._pprofile_prefix.find("?") >= 0):
337
+            raise ValueError(_("pprofile_prefix contains illegal character."))
338
+
339
+    @utils.synchronized(_LOCK_NAME)
340
+    def associate_mac_to_network(self, address, username, password,
341
+                                 vfab_id, net_id, mac):
342
+        """Associates a MAC address to virtual network."""
343
+
344
+        try:
345
+            self.mgr.connect(address, username, password)
346
+            self._associate_mac_to_port_profile(vfab_id, net_id, mac)
347
+        except (EOFError, EnvironmentError, select.error,
348
+                ml2_exc.MechanismDriverError):
349
+            with excutils.save_and_reraise_exception():
350
+                LOG.exception(_LE("CLI error"))
351
+
352
+    @utils.synchronized(_LOCK_NAME)
353
+    def dissociate_mac_from_network(self, address, username, password,
354
+                                    vfab_id, net_id, mac):
355
+        """Dissociates a MAC address from virtual network."""
356
+
357
+        try:
358
+            self.mgr.connect(address, username, password)
359
+            self._dissociate_mac_from_port_profile(vfab_id, net_id, mac)
360
+        except (EOFError, EnvironmentError, select.error,
361
+                ml2_exc.MechanismDriverError):
362
+            with excutils.save_and_reraise_exception():
363
+                LOG.exception(_LE("CLI error"))
364
+
365
+    def _create_port_profile(self, vlan_id, mac_address, running_config=None,
366
+                             commit=True):
367
+        """Creates a port profile."""
368
+
369
+        if running_config is None:
370
+            running_config = self.mgr.get_running_config()
371
+        if self._share_pprofile:
372
+            pprofile = self._get_pprofile(vlan_id, mac_address, running_config)
373
+            if pprofile is None:
374
+                pprofile = self._get_new_pprofile(
375
+                    vlan_id, mac_address, running_config)
376
+                self._configure_pprofile(pprofile, vlan_id, commit)
377
+        else:
378
+            pprofile = self._get_new_pprofile(
379
+                vlan_id, mac_address, running_config)
380
+            match = re.search(
381
+                r"^pprofile\s+{pid}\s+vlan\s+tag\s+([0-9,-]+)".format(
382
+                    pid=re.escape(pprofile)), running_config, re.MULTILINE)
383
+            if match:
384
+                if match.group(1) != str(vlan_id):
385
+                    LOG.warning(
386
+                        _LW('Override "pprofile %(pid)s vlan tag %(vids)s" '
387
+                            'to "vlan tag %(vlan_id)s"'),
388
+                        dict(pid=pprofile, vids=match.group(1),
389
+                             vlan_id=vlan_id))
390
+                    self._configure_pprofile(pprofile, vlan_id, commit)
391
+            else:
392
+                self._configure_pprofile(pprofile, vlan_id, commit)
393
+        return pprofile
394
+
395
+    def _configure_pprofile(self, profile, vlan_id, commit=True):
396
+        """Configures pprofile."""
397
+
398
+        self.mgr.configure(
399
+            ["pprofile {pid} vlan tag {vid}".format(
400
+                pid=profile, vid=vlan_id)], commit=commit)
401
+
402
+    def _get_pprofile(self, vlan_id, mac_address, running_config):
403
+        """Gets the name of existing pprofile."""
404
+
405
+        if self._share_pprofile:
406
+            pprofile = None
407
+            match = re.search(
408
+                r"^pprofile\s+({prefix}\S+)\s+vlan\s+tag\s+{vid}$".format(
409
+                    prefix=re.escape(self._pprofile_prefix), vid=vlan_id),
410
+                running_config, re.MULTILINE)
411
+            if match:
412
+                pprofile = match.group(1)
413
+        else:
414
+            pprofile = self._get_new_pprofile(
415
+                vlan_id, mac_address, running_config)
416
+            match = re.search(
417
+                r"^pprofile\s+{pid}\s+vlan\s+tag\s+{vid}$".format(
418
+                    pid=re.escape(pprofile), vid=vlan_id),
419
+                running_config, re.MULTILINE)
420
+            if not match:
421
+                pprofile = None
422
+        return pprofile
423
+
424
+    def _get_new_pprofile(self, vlan_id, mac_address, running_config):
425
+        """Gets a name of new pprofile for the MAC address or the vlan id."""
426
+
427
+        if self._share_pprofile:
428
+            used = re.findall(
429
+                r"^pprofile\s+(\S+)\s+", running_config, re.MULTILINE)
430
+            while True:
431
+                pprofile = self._pprofile_name.format(
432
+                    prefix=self._pprofile_prefix, pid=vlan_id)
433
+                if pprofile not in used:
434
+                    return pprofile
435
+                vlan_id += 1
436
+        else:
437
+            return self._pprofile_name.format(
438
+                prefix=self._pprofile_prefix, pid=mac_address)
439
+
440
+    def _associate_mac_to_port_profile(self, vfab_id, vlan_id, mac_address):
441
+        """Associates a MAC address to a port profile."""
442
+
443
+        running_config = self.mgr.get_running_config()
444
+        pprofile = self._create_port_profile(
445
+            vlan_id, mac_address, running_config, commit=False)
446
+        index, profile_name = _search_vfab_pprofile(
447
+            vfab_id, mac_address, running_config)
448
+        if index is None:
449
+            index = _get_available_vfab_pprofile_index(
450
+                vfab_id, running_config)
451
+            if index is None:
452
+                LOG.error(_LE("No unused vfab pprofile index"))
453
+                raise ml2_exc.MechanismDriverError(
454
+                    method="_associate_mac_to_port_profile")
455
+        else:
456
+            if pprofile == profile_name:
457
+                return
458
+            else:
459
+                LOG.warning(
460
+                    _LW('Override "vfab %(vfab_id)s pprofile %(index)d vsiid '
461
+                        'mac %(mac)s %(profile_name)s" to "vsiid mac %(mac)s '
462
+                        '%(pprofile)s"'),
463
+                    dict(vfab_id=vfab_id, index=index, mac=mac_address,
464
+                         profile_name=profile_name, pprofile=pprofile))
465
+        self.mgr.configure(
466
+            ["vfab {vfab_id} pprofile {index} vsiid mac {mac} "
467
+             "{pid}".format(
468
+                 vfab_id=vfab_id, index=index, mac=mac_address, pid=pprofile)])
469
+
470
+    def _dissociate_mac_from_port_profile(self, vfab_id, vlan_id, mac_address):
471
+        """Dissociates a MAC address from a port profile."""
472
+
473
+        running_config = self.mgr.get_running_config()
474
+        pprofile = self._get_pprofile(vlan_id, mac_address, running_config)
475
+        if pprofile is None:
476
+            return
477
+        index = _get_vfab_pprofile_index(
478
+            vfab_id, pprofile, mac_address, running_config)
479
+        if index is not None:
480
+            delete_port_profile = True
481
+            for m in _VFAB_PPROFILE_RE.finditer(running_config):
482
+                if m.group(3) == pprofile and not (
483
+                        m.group(1) == vfab_id and m.group(2) == index):
484
+                    delete_port_profile = False
485
+                    break
486
+            self.mgr.configure(
487
+                ["no vfab {vfab_id} pprofile {index}".format(
488
+                    vfab_id=vfab_id, index=index)],
489
+                commit=not delete_port_profile)
490
+            if delete_port_profile:
491
+                self.mgr.configure(["no pprofile {pid}".format(pid=pprofile)])
492
+        else:
493
+            LOG.warning(
494
+                _LW("No corresponding vfab pprofile for %(vid)s, %(mac)s"),
495
+                dict(vid=vlan_id, mac=mac_address))
496
+
497
+
498
+def _search_vfab_pprofile(vfab_id, mac_address, running_config):
499
+    """Search for the vfab pprofile. Returns (index, pprofile) if found."""
500
+
501
+    match = re.search(
502
+        r"^vfab\s+{vfab_id}\s+pprofile\s+(\d+)\s+"
503
+        r"vsiid\s+mac\s+{mac}\s+(\S+)$".format(
504
+            vfab_id=vfab_id, mac=mac_address),
505
+        running_config, re.MULTILINE)
506
+    if match:
507
+        return int(match.group(1)), match.group(2)
508
+    else:
509
+        return None, None
510
+
511
+
512
+def _get_vfab_pprofile_index(vfab_id, pprofile, mac_address, running_config):
513
+    """Gets the index for vfab pprofile."""
514
+
515
+    index = None
516
+    match = re.search(
517
+        r"^vfab\s+{vfab_id}\s+pprofile\s+(\d+)\s+"
518
+        r"vsiid\s+mac\s+{mac}\s+{pid}\b".format(
519
+            vfab_id=vfab_id, mac=mac_address, pid=pprofile),
520
+        running_config, re.MULTILINE)
521
+    if match:
522
+        index = match.group(1)
523
+    return index
524
+
525
+
526
+def _get_available_vfab_pprofile_index(vfab_id, running_config):
527
+    """Gets an available index for vfab pprofile."""
528
+
529
+    available = _PPROFILE_INDICES - set(
530
+        [int(x) for x in
531
+         re.findall(
532
+             r"^vfab\s+{vfab_id}\s+pprofile\s+(\d+)\s+".format(
533
+                 vfab_id=vfab_id), running_config, re.MULTILINE)])
534
+    if len(available) > 0:
535
+        return sorted(available)[0]
536
+    else:
537
+        return None

+ 292
- 0
networking_fujitsu/ml2/drivers/fujitsu/cfab/mechanism_fujitsu.py View File

@@ -0,0 +1,292 @@
1
+# Copyright 2015 FUJITSU LIMITED
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
+
16
+
17
+"""Implementation of Fujitsu ML2 Mechanism driver for ML2 Plugin."""
18
+
19
+try:
20
+    from oslo_config import cfg
21
+except ImportError:
22
+    from oslo.config import cfg
23
+try:
24
+    from oslo_log import log as logging
25
+except ImportError:
26
+    from neutron.openstack.common import log as logging
27
+try:
28
+    from oslo_utils import importutils
29
+except ImportError:
30
+    from neutron.openstack.common import importutils
31
+try:
32
+    from neutron.i18n import _LE
33
+    from neutron.i18n import _LI
34
+except ImportError:
35
+    try:
36
+        from neutron.openstack.common._i18n import _LE
37
+        from neutron.openstack.common._i18n import _LI
38
+    except ImportError:
39
+        from neutron.openstack.common.gettextutils import _LE
40
+        from neutron.openstack.common.gettextutils import _LI
41
+from neutron.plugins.ml2.common import exceptions as ml2_exc
42
+from neutron.plugins.ml2 import driver_api
43
+
44
+LOG = logging.getLogger(__name__)
45
+FUJITSU_DRIVER = 'networking_fujitsu.ml2.drivers.fujitsu.'
46
+CFAB_DRIVER = FUJITSU_DRIVER + 'cfab.cfabdriver.CFABdriver'
47
+
48
+VFAB_ID_DEFAULT = "default"
49
+VFAB_ID_MIN = 1
50
+VFAB_ID_MAX = 3000
51
+
52
+ML2_FUJITSU_GROUP = "fujitsu_cfab"
53
+ML2_FUJITSU = [
54
+    cfg.StrOpt(
55
+        'address', default='',
56
+        help=_("The address of the C-Fabric to telnet to.")),
57
+    cfg.StrOpt(
58
+        'username', default='admin',
59
+        help=_("The C-Fabric username to use.")),
60
+    cfg.StrOpt(
61
+        'password', default='admin', secret=True,
62
+        help=_("The C-Fabric password to use.")),
63
+    cfg.ListOpt(
64
+        'physical_networks', default='',
65
+        help=_("List of <physical_network>:<vfab_id> tuples specifying "
66
+               "physical_network names and corresponding vfab ids.")),
67
+    cfg.BoolOpt(
68
+        'share_pprofile', default=False,
69
+        help=_("Whether to share a C-Fabric pprofile among Neutron "
70
+               "ports using the same VLAN ID.")),
71
+    cfg.StrOpt(
72
+        'pprofile_prefix', default='',
73
+        help=_("The prefix string for pprofile name.")),
74
+    cfg.BoolOpt(
75
+        'save_config', default=True,
76
+        help=_("Whether to save configuration."))]
77
+
78
+cfg.CONF.register_opts(ML2_FUJITSU, ML2_FUJITSU_GROUP)
79
+
80
+
81
+class FujitsuMechanism(driver_api.MechanismDriver):
82
+    """ML2 Mechanism driver for Fujitsu C-Fabric switches. This is the upper
83
+    layer driver class that interfaces to lower layer (CLI) below.
84
+    """
85
+
86
+    def __init__(self):
87
+        self._driver = None
88
+        self._physical_networks = {}
89
+        self._switch = None
90
+        self.initialize()
91
+
92
+    def initialize(self):
93
+        """Initialize of variables needed by this class."""
94
+
95
+        self._parse_physical_networks()
96
+        self._switch = {'address': cfg.CONF.fujitsu_cfab.address,
97
+                        'username': cfg.CONF.fujitsu_cfab.username,
98
+                        'password': cfg.CONF.fujitsu_cfab.password
99
+                        }
100
+
101
+        if not self._switch['address']:
102
+            raise cfg.RequiredOptError(
103
+                'address', cfg.OptGroup(ML2_FUJITSU_GROUP))
104
+
105
+        self._driver = importutils.import_object(CFAB_DRIVER, cfg.CONF)
106
+
107
+    def _parse_physical_networks(self):
108
+        """Interpret physical_networks as physical_network:vfab_id entries."""
109
+
110
+        for entry in cfg.CONF.fujitsu_cfab.physical_networks:
111
+            try:
112
+                physical_network, vfab_id = entry.split(':')
113
+            except ValueError:
114
+                LOG.exception(
115
+                    _LE("Fujitsu Mechanism: illegal physical_networks entry")
116
+                )
117
+                raise ml2_exc.MechanismDriverError(
118
+                    method="_parse_physical_networks")
119
+            if not (vfab_id == VFAB_ID_DEFAULT or
120
+                    VFAB_ID_MIN <= int(vfab_id) <= VFAB_ID_MAX):
121
+                LOG.error(
122
+                    _LE("Fujitsu Mechanism: illegal vfab id in "
123
+                        "physical_networks entry")
124
+                )
125
+                raise ml2_exc.MechanismDriverError(
126
+                    method="_parse_physical_networks")
127
+            self._physical_networks[physical_network] = vfab_id
128
+
129
+    def _get_vfab_id(self, physical_network):
130
+        """Get vfab_id corresponding to the physical_network."""
131
+
132
+        try:
133
+            vfab_id = self._physical_networks[physical_network]
134
+        except KeyError:
135
+            LOG.exception(
136
+                _LE("Fujitsu Mechanism: network cannot be found in the "
137
+                    "configured physical network"))
138
+            raise ml2_exc.MechanismDriverError(method="_get_vfab_id")
139
+        return vfab_id
140
+
141
+    def create_network_precommit(self, mech_context):
142
+        """Noop now, it is left here for future."""
143
+        LOG.debug("create_network_precommit: called")
144
+
145
+    def create_network_postcommit(self, mech_context):
146
+        """Noop now, it is left here for future."""
147
+        LOG.debug("create_network_postcommit: called")
148
+
149
+    def delete_network_precommit(self, mech_context):
150
+        """Noop now, it is left here for future."""
151
+        LOG.debug("delete_network_precommit: called")
152
+
153
+    def delete_network_postcommit(self, mech_context):
154
+        """Noop now, it is left here for future."""
155
+        LOG.debug("delete_network_postcommit: called")
156
+
157
+    def update_network_precommit(self, mech_context):
158
+        """Noop now, it is left here for future."""
159
+        LOG.debug("update_network_precommit(self: called")
160
+
161
+    def update_network_postcommit(self, mech_context):
162
+        """Noop now, it is left here for future."""
163
+        LOG.debug("update_network_postcommit(self: called")
164
+
165
+    def create_port_precommit(self, mech_context):
166
+        """Noop now, it is left here for future."""
167
+        LOG.debug("create_port_precommit: called")
168
+
169
+    def create_port_postcommit(self, mech_context):
170
+        """Associate the assigned MAC address to the portprofile."""
171
+
172
+        LOG.debug("create_port_postcommit: called")
173
+
174
+        port = mech_context.current
175
+        port_id = port['id']
176
+        network_id = port['network_id']
177
+        tenant_id = port['tenant_id']
178
+
179
+        segments = mech_context.network.network_segments
180
+        # currently supports only one segment per network
181
+        segment = segments[0]
182
+        _validate_network_type(
183
+            segment[driver_api.NETWORK_TYPE],
184
+            method="create_port_postcommit")
185
+        vfab_id = self._get_vfab_id(segment[driver_api.PHYSICAL_NETWORK])
186
+        vlan_id = segment[driver_api.SEGMENTATION_ID]
187
+
188
+        interface_mac = port['mac_address']
189
+
190
+        try:
191
+            self._driver.associate_mac_to_network(self._switch['address'],
192
+                                                  self._switch['username'],
193
+                                                  self._switch['password'],
194
+                                                  vfab_id,
195
+                                                  vlan_id,
196
+                                                  interface_mac)
197
+        except Exception:
198
+            LOG.exception(
199
+                _LE("Fujitsu Mechanism: failed to associate mac %s")
200
+                % interface_mac)
201
+            raise ml2_exc.MechanismDriverError(
202
+                method="create_port_postcommit")
203
+
204
+        LOG.info(
205
+            _LI("created port (postcommit): port_id=%(port_id)s "
206
+                "network_id=%(network_id)s tenant_id=%(tenant_id)s"),
207
+            {'port_id': port_id,
208
+             'network_id': network_id, 'tenant_id': tenant_id})
209
+
210
+    def delete_port_precommit(self, mech_context):
211
+        """Noop now, it is left here for future."""
212
+        LOG.debug("delete_port_precommit: called")
213
+
214
+    def delete_port_postcommit(self, mech_context):
215
+        """Dissociate MAC address from the portprofile."""
216
+
217
+        LOG.debug("delete_port_postcommit: called")
218
+        port = mech_context.current
219
+        port_id = port['id']
220
+        network_id = port['network_id']
221
+        tenant_id = port['tenant_id']
222
+
223
+        segments = mech_context.network.network_segments
224
+        # currently supports only one segment per network
225
+        segment = segments[0]
226
+        _validate_network_type(
227
+            segment[driver_api.NETWORK_TYPE],
228
+            method="delete_port_postcommit")
229
+        vfab_id = self._get_vfab_id(segment[driver_api.PHYSICAL_NETWORK])
230
+        vlan_id = segment[driver_api.SEGMENTATION_ID]
231
+
232
+        interface_mac = port['mac_address']
233
+
234
+        try:
235
+            self._driver.dissociate_mac_from_network(
236
+                self._switch['address'],
237
+                self._switch['username'],
238
+                self._switch['password'],
239
+                vfab_id,
240
+                vlan_id,
241
+                interface_mac)
242
+        except Exception:
243
+            LOG.exception(
244
+                _LE("Fujitsu Mechanism: failed to dissociate MAC %s") %
245
+                interface_mac)
246
+            raise ml2_exc.MechanismDriverError(
247
+                method="delete_port_postcommit")
248
+
249
+        LOG.info(
250
+            _LI("delete port (postcommit): port_id=%(port_id)s "
251
+                "network_id=%(network_id)s tenant_id=%(tenant_id)s"),
252
+            {'port_id': port_id,
253
+             'network_id': network_id, 'tenant_id': tenant_id})
254
+
255
+    def update_port_precommit(self, mech_context):
256
+        """Noop now, it is left here for future."""
257
+        LOG.debug("update_port_precommit(self: called")
258
+
259
+    def update_port_postcommit(self, mech_context):
260
+        """Noop now, it is left here for future."""
261
+        LOG.debug("update_port_postcommit: called")
262
+
263
+    def create_subnet_precommit(self, mech_context):
264
+        """Noop now, it is left here for future."""
265
+        LOG.debug("create_subnetwork_precommit: called")
266
+
267
+    def create_subnet_postcommit(self, mech_context):
268
+        """Noop now, it is left here for future."""
269
+        LOG.debug("create_subnetwork_postcommit: called")
270
+
271
+    def delete_subnet_precommit(self, mech_context):
272
+        """Noop now, it is left here for future."""
273
+        LOG.debug("delete_subnetwork_precommit: called")
274
+
275
+    def delete_subnet_postcommit(self, mech_context):
276
+        """Noop now, it is left here for future."""
277
+        LOG.debug("delete_subnetwork_postcommit: called")
278
+
279
+    def update_subnet_precommit(self, mech_context):
280
+        """Noop now, it is left here for future."""
281
+        LOG.debug("update_subnet_precommit(self: called")
282
+
283
+    def update_subnet_postcommit(self, mech_context):
284
+        """Noop now, it is left here for future."""
285
+        LOG.debug("update_subnet_postcommit: called")
286
+
287
+
288
+def _validate_network_type(network_type, method="_validate_network_type"):
289
+    if network_type != 'vlan':
290
+        LOG.error(
291
+            _LE("Fujitsu Mechanism: only network type vlan is supported"))
292
+        raise ml2_exc.MechanismDriverError(method=method)

+ 1
- 1
networking_fujitsu/tests/base.py View File

@@ -20,4 +20,4 @@ from oslotest import base
20 20
 
21 21
 class TestCase(base.BaseTestCase):
22 22
 
23
-    """Test case base class for all unit tests."""
23
+    """Test case base class for all unit tests."""

+ 1
- 1
networking_fujitsu/tests/test_networking_fujitsu.py View File

@@ -25,4 +25,4 @@ from networking_fujitsu.tests import base
25 25
 class TestNetworking_fujitsu(base.TestCase):
26 26
 
27 27
     def test_something(self):
28
-        pass
28
+        pass

+ 0
- 0
networking_fujitsu/tests/unit/__init__.py View File


+ 0
- 0
networking_fujitsu/tests/unit/ml2/__init__.py View File


+ 0
- 0
networking_fujitsu/tests/unit/ml2/drivers/__init__.py View File


+ 0
- 0
networking_fujitsu/tests/unit/ml2/drivers/fujitsu/__init__.py View File


+ 0
- 0
networking_fujitsu/tests/unit/ml2/drivers/fujitsu/cfab/__init__.py View File


+ 682
- 0
networking_fujitsu/tests/unit/ml2/drivers/fujitsu/cfab/test_fujitsu_cfabdriver.py View File

@@ -0,0 +1,682 @@
1
+# Copyright 2015 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain 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,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+# See the License for the specific language governing permissions and
14
+# limitations under the License.
15
+
16
+import os
17
+import re
18
+import socket
19
+
20
+import mock
21
+import testtools
22
+
23
+from networking_fujitsu.ml2.drivers.fujitsu.cfab import cfabdriver
24
+from networking_fujitsu.ml2.drivers.fujitsu.cfab.mechanism_fujitsu import cfg
25
+from neutron.common import utils
26
+from neutron.plugins.ml2.common import exceptions as ml2_exc
27
+from neutron.plugins.ml2 import config as ml2_config
28
+from neutron.tests import base
29
+
30
+FUJITSU_CFAB = "networking_fujitsu.ml2.drivers.fujitsu.cfab."
31
+_CFABDRIVER__CFABMANAGER = FUJITSU_CFAB + "cfabdriver._CFABManager"
32
+_TELNETLIB_TELNET = FUJITSU_CFAB + "cfabdriver.telnetlib.Telnet"
33
+
34
+_EXCLUDE_BRACKET_LINE_RE = re.compile(r"^[^[].*$", re.MULTILINE)
35
+
36
+
37
+class BaseTestMockedCFABManager(base.BaseTestCase):
38
+    """Base class to test Fujitsu C-Fabric manager.
39
+    """
40
+
41
+    def setUp(self):
42
+        super(BaseTestMockedCFABManager, self).setUp()
43
+        self.manager = cfabdriver._CFABManager()
44
+
45
+    def assert_wrote(self, lines):
46
+        telnet = self.manager._telnet
47
+        """:type : mock.MagicMock"""
48
+        self.assertEqual(
49
+            lines, [x[0][0] for x in telnet.write.call_args_list])
50
+
51
+
52
+class TestMockedCFABManager(BaseTestMockedCFABManager):
53
+    """Test Fujitsu C-Fabric manager.
54
+    """
55
+
56
+    def test_connect(self):
57
+        with mock.patch(_TELNETLIB_TELNET, autospec=True) as telnet:
58
+
59
+            self.manager.connect("address", "username", "password")
60
+
61
+        telnet.assert_called_once_with(
62
+            host="address",
63
+            port=cfabdriver.TELNET_PORT,
64
+            timeout=cfabdriver._TIMEOUT)
65
+        self.assert_wrote(["username\n", "password\n"])
66
+
67
+    def test_connect_fail(self):
68
+        with mock.patch(_TELNETLIB_TELNET, autospec=True) as telnet:
69
+            telnet.side_effect = socket.error
70
+
71
+            self.assertRaises(
72
+                socket.error,
73
+                self.manager.connect, "address", "username", "password")
74
+
75
+
76
+class BaseTestMockedCFABManagerConnected(BaseTestMockedCFABManager):
77
+    """Base class to test Fujitsu C-Fabric manager after connected.
78
+    """
79
+
80
+    def setUp(self):
81
+        super(BaseTestMockedCFABManagerConnected, self).setUp()
82
+        with mock.patch(_TELNETLIB_TELNET, autospec=True):
83
+            self.manager.connect("address", "username", "password")
84
+        self.prompt = "# "
85
+
86
+        def read_until(*args, **kwargs):
87
+            return "(config)# "
88
+
89
+        def expect(*args, **kwargs):
90
+            s = self.prompt
91
+            m = args[0][0].search(s)
92
+            return 0 if m is not None else -1, m, s
93
+
94
+        self.manager._telnet.read_until.side_effect = read_until
95
+        self.manager._telnet.expect.side_effect = expect
96
+
97
+
98
+class TestMockedCFABManagerConnected(BaseTestMockedCFABManagerConnected):
99
+    """Test Fujitsu C-Fabric manager after connected.
100
+    """
101
+
102
+    def test_get_running_config(self):
103
+        running_config = self.manager.get_running_config()
104
+
105
+        self.assertEqual(
106
+            mock.call("show running-config\n"),
107
+            self.manager._telnet.write.call_args)
108
+        self.assertEqual("", running_config)
109
+
110
+    def test_configure(self):
111
+        cmd = "pprofile 1 vlan tag 1"
112
+
113
+        self.manager.configure([cmd])
114
+
115
+        call_args_list = self.manager._telnet.write.call_args_list
116
+        self.assertIn(mock.call("configure\n"), call_args_list)
117
+        self.assertIn(mock.call(cmd + "\n"), call_args_list)
118
+        self.assertIn(mock.call("commit\n"), call_args_list)
119
+        self.assertEqual(
120
+            mock.call("save\n"), self.manager._telnet.write.call_args)
121
+
122
+    def test_configure_without_commit(self):
123
+        cmd = "pprofile 1 vlan tag 1"
124
+
125
+        self.manager.configure([cmd], commit=False)
126
+
127
+        call_args_list = self.manager._telnet.write.call_args_list
128
+        self.assertIn(mock.call("configure\n"), call_args_list)
129
+        self.assertIn(mock.call(cmd + "\n"), call_args_list)
130
+        self.assertNotIn(mock.call("commit\n"), call_args_list)
131
+
132
+
133
+class TestMockedCFABManagerConnectedWithoutSave(
134
+        BaseTestMockedCFABManagerConnected):
135
+    """Test Fujitsu C-Fabric manager after connected without save.
136
+    """
137
+
138
+    def setUp(self):
139
+        super(TestMockedCFABManagerConnectedWithoutSave, self).setUp()
140
+        self.manager.save_config = False
141
+
142
+    def test_configure_without_save(self):
143
+        cmd = "pprofile 1 vlan tag 1"
144
+
145
+        self.manager.configure([cmd])
146
+
147
+        call_args_list = self.manager._telnet.write.call_args_list
148
+        self.assertIn(mock.call("configure\n"), call_args_list)
149
+        self.assertIn(mock.call(cmd + "\n"), call_args_list)
150
+        self.assertEqual(
151
+            mock.call("commit\n"), self.manager._telnet.write.call_args)
152
+        self.assertNotIn(mock.call("save\n"), call_args_list)
153
+
154
+
155
+@testtools.skipUnless(
156
+    'OS_FUJITSU_CFAB_ADDRESS' in os.environ,
157
+    "OS_FUJITSU_CFAB_ADDRESS environment variable is not defined.")
158
+class TestCFABManager(base.BaseTestCase):
159
+    """Test Fujitsu C-Fabric manager using the real telnet connection.
160
+
161
+    Tests will be performed using the C-Fabric CLI through a telnet connection
162
+    to the address OS_FUJITSU_CFAB_ADDRESS and the port OS_FUJITSU_CFAB_PORT
163
+    (defaults to 23). The username will be taken from OS_FUJITSU_CFAB_USERNAME
164
+    (defaults to "admin") and the password will be taken from
165
+    OS_FUJITSU_CFAB_PASSWORD (defaults to "password").
166
+    If the environment variable OS_FUJITSU_CFAB_ADDRESS is NOT defined, tests
167
+    will be skipped.
168
+    """
169
+
170
+    def _setup_lock(self):
171
+        """Set up lock_path so that all tests are serialized.
172
+
173
+        This is necessary to keep the C-Fabric config consistent within each
174
+        test.
175
+        """
176
+
177
+        try:
178
+            ml2_config.cfg.CONF.set_override('lock_path', "lock")
179
+        except ml2_config.cfg.NoSuchOptError:
180
+            ml2_config.cfg.CONF.set_override(
181
+                'lock_path', "lock", "oslo_concurrency")
182
+
183
+    def setUp(self):
184
+        super(TestCFABManager, self).setUp()
185
+        self._setup_lock()
186
+        try:
187
+            cfabdriver.TELNET_PORT = int(os.environ['OS_FUJITSU_CFAB_PORT'])
188
+        except KeyError:
189
+            pass
190
+        self.manager = cfabdriver.CFAB_MANAGER
191
+        self.manager.connect(
192
+            os.environ.get('OS_FUJITSU_CFAB_ADDRESS'),
193
+            os.environ.get('OS_FUJITSU_CFAB_USERNAME') or "admin",
194
+            os.environ.get('OS_FUJITSU_CFAB_PASSWORD') or "password",
195
+        )
196
+
197
+    def assert_running_config(self, prefix, expected_config):
198
+        running_config = self.manager.get_running_config(prefix=prefix)
199
+        self.assertEqual(
200
+            expected_config, _EXCLUDE_BRACKET_LINE_RE.findall(running_config))
201
+
202
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
203
+    def test_modes(self):
204
+        self.manager._close_session()
205
+        self.assertEqual(cfabdriver._MODE_ADMIN, self.manager._get_mode())
206
+        self.manager._execute("configure")
207
+        self.assertEqual(cfabdriver._MODE_CONFIG, self.manager._get_mode())
208
+        self.manager._execute("interface 1/1/1/1")
209
+        self.assertEqual(cfabdriver._MODE_CONFIG_IF, self.manager._get_mode())
210
+        self.manager._execute("exit")
211
+        self.assertEqual(cfabdriver._MODE_CONFIG, self.manager._get_mode())
212
+        self.manager._execute("exit")
213
+        self.assertEqual(cfabdriver._MODE_ADMIN, self.manager._get_mode())
214
+        self.manager._execute("exit")
215
+        self.assertEqual(cfabdriver._MODE_USER, self.manager._get_mode())
216
+
217
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
218
+    def test_get_running_config(self):
219
+        self.manager.configure(
220
+            ["no pprofile",
221
+             "pprofile 1 vlan tag 1",
222
+             "pprofile 2 vlan tag 2"])
223
+
224
+        running_config = self.manager.get_running_config()
225
+
226
+        self.assertEqual(
227
+            ["pprofile 1 vlan tag 1", "pprofile 2 vlan tag 2"],
228
+            re.findall(r"^pprofile\s+.+$", running_config, re.MULTILINE))
229
+
230
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
231
+    def test_get_running_config_prefix(self):
232
+        self.manager.configure(
233
+            ["no pprofile",
234
+             "pprofile 1 vlan tag 1",
235
+             "pprofile 2 vlan tag 2"])
236
+
237
+        self.assert_running_config(
238
+            "pprofile", ["1 vlan tag 1", "2 vlan tag 2"])
239
+
240
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
241
+    def test_configure(self):
242
+        self.manager.configure(["no pprofile"])
243
+
244
+        self.assert_running_config("pprofile", [])
245
+
246
+        self.manager.configure(["pprofile 1 vlan tag 1"])
247
+
248
+        self.assert_running_config("pprofile", ["1 vlan tag 1"])
249
+
250
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
251
+    def test_configure_from_interface_config(self):
252
+        self.manager.configure(["no pprofile"])
253
+        self.manager._execute("interface 1/1/1/1")
254
+
255
+        self.manager.configure(["pprofile 1 vlan tag 1"])
256
+
257
+        self.assert_running_config("pprofile", ["1 vlan tag 1"])
258
+
259
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
260
+    def test_configure_from_user(self):
261
+        self.manager.configure(["no pprofile"])
262
+        self.manager._execute("exit")
263
+        self.manager._execute("exit")
264
+
265
+        self.manager.configure(["pprofile 1 vlan tag 1"])
266
+
267
+        self.assert_running_config("pprofile", ["1 vlan tag 1"])
268
+
269
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
270
+    def test_configure_from_closed(self):
271
+        self.manager.configure(["no pprofile"])
272
+        self.manager._close_session()
273
+
274
+        self.manager.configure(["pprofile 1 vlan tag 1"])
275
+
276
+        self.assert_running_config("pprofile", ["1 vlan tag 1"])
277
+
278
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
279
+    def test_configure_no_commit(self):
280
+        self.manager.configure(["no pprofile"])
281
+
282
+        self.manager.configure(["pprofile 1 vlan tag 1"], commit=False)
283
+
284
+        self.assert_running_config("pprofile", [])
285
+
286
+        self.manager.configure([])
287
+
288
+        self.assert_running_config("pprofile", ["1 vlan tag 1"])
289
+
290
+    @utils.synchronized(cfabdriver._LOCK_NAME, external=True)
291
+    def test_configure_error(self):
292
+        self.assertRaises(
293
+            ml2_exc.MechanismDriverError, self.manager.configure, ["error"])
294
+
295
+
296
+class BaseTestCFABdriver(base.BaseTestCase):
297
+    """Base class to test Fujitsu C-Fabric mechanism driver.
298
+    """
299
+
300
+    def setUp(self):
301
+        super(BaseTestCFABdriver, self).setUp()
302
+        with mock.patch(_CFABDRIVER__CFABMANAGER, autospec=True) as mocked:
303
+            self.driver = cfabdriver.CFABdriver(cfg.CONF)
304
+            self.driver.mgr = mocked.return_value
305
+
306
+    def assert_configured(self, cmds):
307
+        mgr = self.driver.mgr
308
+        """:type : mock.MagicMock"""
309
+        self.assertEqual(
310
+            cmds, [x[0][0][0] for x in mgr.configure.call_args_list])
311
+
312
+        # Make sure that only the last configure has commit=True.
313
+        commits = [x[1].get('commit', True)
314
+                   for x in mgr.configure.call_args_list]
315
+        self.assertTrue(commits.pop())
316
+        commits.append(False)
317
+        self.assertEqual({False}, set(commits))
318
+
319
+
320
+class TestCFABdriver(BaseTestCFABdriver):
321
+    """Test Fujitsu C-Fabric mechanism driver.
322
+    """
323
+
324
+    def test_associate_mac_to_network(self):
325
+        mgr = self.driver.mgr
326
+        """:type : mock.MagicMock"""
327
+        mgr.get_running_config.return_value = (
328
+            """pprofile 00:01:02:03:04:05 vlan tag 2
329
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
330
+""")
331
+
332
+        self.driver.associate_mac_to_network(
333
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
334
+
335
+        mgr.connect.assert_called_once_with("address", "username", "password")
336
+        mgr.get_running_config.assert_called_once_with()
337
+        self.assert_configured(
338
+            ["vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 "
339
+             "00:01:02:03:04:05"])
340
+
341
+    def test_associate_mac_to_network_no_pprofile(self):
342
+        mgr = self.driver.mgr
343
+        """:type : mock.MagicMock"""
344
+        mgr.get_running_config.return_value = ""
345
+
346
+        self.driver.associate_mac_to_network(
347
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
348
+
349
+        mgr.connect.assert_called_once_with("address", "username", "password")
350
+        mgr.get_running_config.assert_called_once_with()
351
+        self.assert_configured(
352
+            ["pprofile 00:01:02:03:04:05 vlan tag 2",
353
+             "vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 "
354
+             "00:01:02:03:04:05"])
355
+
356
+    def test_associate_mac_to_network_existing(self):
357
+        mgr = self.driver.mgr
358
+        """:type : mock.MagicMock"""
359
+        mgr.get_running_config.return_value = (
360
+            """pprofile 00:01:02:03:04:05 vlan tag 2
361
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
362
+""")
363
+
364
+        self.driver.associate_mac_to_network(
365
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
366
+
367
+        mgr.connect.assert_called_once_with("address", "username", "password")
368
+        mgr.get_running_config.assert_called_once_with()
369
+        self.assertFalse(mgr.configure.called)
370
+
371
+    def test_associate_mac_to_network_existing_override(self):
372
+        mgr = self.driver.mgr
373
+        """:type : mock.MagicMock"""
374
+        mgr.get_running_config.return_value = (
375
+            """pprofile test-2 vlan tag 2
376
+vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 test-2
377
+""")
378
+
379
+        self.driver.associate_mac_to_network(
380
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
381
+
382
+        mgr.connect.assert_called_once_with("address", "username", "password")
383
+        mgr.get_running_config.assert_called_once_with()
384
+        self.assert_configured(
385
+            ["pprofile 00:01:02:03:04:05 vlan tag 2",
386
+             "vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 "
387
+             "00:01:02:03:04:05"])
388
+
389
+    def test_associate_mac_to_network_override_pprofile(self):
390
+        mgr = self.driver.mgr
391
+        """:type : mock.MagicMock"""
392
+        mgr.get_running_config.return_value = (
393
+            """pprofile 00:01:02:03:04:05 vlan tag 1,2
394
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
395
+""")
396
+
397
+        self.driver.associate_mac_to_network(
398
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
399
+
400
+        mgr.connect.assert_called_once_with("address", "username", "password")
401
+        mgr.get_running_config.assert_called_once_with()
402
+        self.assert_configured(
403
+            ["pprofile 00:01:02:03:04:05 vlan tag 2",
404
+             "vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 "
405
+             "00:01:02:03:04:05"])
406
+
407
+    def test_dissociate_mac_from_network(self):
408
+        mgr = self.driver.mgr
409
+        """:type : mock.MagicMock"""
410
+        mgr.get_running_config.return_value = (
411
+            """pprofile 00:01:02:03:04:05 vlan tag 2
412
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
413
+""")
414
+
415
+        self.driver.dissociate_mac_from_network(
416
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
417
+
418
+        mgr.connect.assert_called_once_with("address", "username", "password")
419
+        mgr.get_running_config.assert_called_once_with()
420
+        self.assert_configured(
421
+            ["no vfab 3 pprofile 0",
422
+             "no pprofile 00:01:02:03:04:05"])
423
+
424
+    def test_dissociate_mac_from_network_still_used_in_other_vfab(self):
425
+        mgr = self.driver.mgr
426
+        """:type : mock.MagicMock"""
427
+        mgr.get_running_config.return_value = (
428
+            """pprofile 00:01:02:03:04:05 vlan tag 2
429
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
430
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:05 00:01:02:03:04:05
431
+""")
432
+
433
+        self.driver.dissociate_mac_from_network(
434
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
435
+
436
+        mgr.connect.assert_called_once_with("address", "username", "password")
437
+        mgr.get_running_config.assert_called_once_with()
438
+        self.assert_configured(["no vfab 3 pprofile 0"])
439
+
440
+
441
+class TestCFABdriverSharePprofile(BaseTestCFABdriver):
442
+    """Test Fujitsu C-Fabric mechanism driver with shared pprofile.
443
+    """
444
+
445
+    def setUp(self):
446
+        cfg.CONF.set_override('share_pprofile', True, "fujitsu_cfab")
447
+        super(TestCFABdriverSharePprofile, self).setUp()
448
+
449
+    def test_associate_mac_to_network(self):
450
+        mgr = self.driver.mgr
451
+        """:type : mock.MagicMock"""
452
+        mgr.get_running_config.return_value = """pprofile 2 vlan tag 2
453
+vfab 3 pprofile 0 vsiid mac 00:00:00:00:00:01 2
454
+"""
455
+
456
+        self.driver.associate_mac_to_network(
457
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
458
+
459
+        mgr.connect.assert_called_once_with("address", "username", "password")
460
+        mgr.get_running_config.assert_called_once_with()
461
+        self.assert_configured(
462
+            ["vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 2"])
463
+
464
+    def test_associate_mac_to_network_no_pprofile(self):
465
+        mgr = self.driver.mgr
466
+        """:type : mock.MagicMock"""
467
+        mgr.get_running_config.return_value = ""
468
+
469
+        self.driver.associate_mac_to_network(
470
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
471
+
472
+        mgr.connect.assert_called_once_with("address", "username", "password")
473
+        mgr.get_running_config.assert_called_once_with()
474
+        self.assert_configured(
475
+            ["pprofile 2 vlan tag 2",
476
+             "vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 2"])
477
+
478
+    def test_associate_mac_to_network_existing(self):
479
+        mgr = self.driver.mgr
480
+        """:type : mock.MagicMock"""
481
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
482
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
483
+"""
484
+
485
+        self.driver.associate_mac_to_network(
486
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
487
+
488
+        mgr.connect.assert_called_once_with("address", "username", "password")
489
+        mgr.get_running_config.assert_called_once_with()
490
+        self.assertFalse(mgr.configure.called)
491
+
492
+    def test_associate_mac_to_network_existing_override(self):
493
+        mgr = self.driver.mgr
494
+        """:type : mock.MagicMock"""
495
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 4
496
+vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 1
497
+"""
498
+
499
+        self.driver.associate_mac_to_network(
500
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
501
+
502
+        mgr.connect.assert_called_once_with("address", "username", "password")
503
+        mgr.get_running_config.assert_called_once_with()
504
+        self.assert_configured(
505
+            ["pprofile 2 vlan tag 2",
506
+             "vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 2"])
507
+
508
+    def test_dissociate_mac_from_network(self):
509
+        mgr = self.driver.mgr
510
+        """:type : mock.MagicMock"""
511
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
512
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
513
+"""
514
+
515
+        self.driver.dissociate_mac_from_network(
516
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
517
+
518
+        mgr.connect.assert_called_once_with("address", "username", "password")
519
+        mgr.get_running_config.assert_called_once_with()
520
+        self.assert_configured(
521
+            ["no vfab 3 pprofile 0",
522
+             "no pprofile 1"])
523
+
524
+    def test_dissociate_mac_from_network_still_used(self):
525
+        mgr = self.driver.mgr
526
+        """:type : mock.MagicMock"""
527
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
528
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
529
+vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:06 1
530
+"""
531
+
532
+        self.driver.dissociate_mac_from_network(
533
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
534
+
535
+        mgr.connect.assert_called_once_with("address", "username", "password")
536
+        mgr.get_running_config.assert_called_once_with()
537
+        self.assert_configured(["no vfab 3 pprofile 0"])
538
+
539
+    def test_dissociate_mac_from_network_still_used_in_other_vfab(self):
540
+        mgr = self.driver.mgr
541
+        """:type : mock.MagicMock"""
542
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
543
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
544
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:06 1
545
+"""
546
+
547
+        self.driver.dissociate_mac_from_network(
548
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
549
+
550
+        mgr.connect.assert_called_once_with("address", "username", "password")
551
+        mgr.get_running_config.assert_called_once_with()
552
+        self.assert_configured(["no vfab 3 pprofile 0"])
553
+
554
+    def test_dissociate_mac_from_network_no_match(self):
555
+        mgr = self.driver.mgr
556
+        """:type : mock.MagicMock"""
557
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 4
558
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
559
+"""
560
+
561
+        self.driver.dissociate_mac_from_network(
562
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
563
+
564
+        mgr.connect.assert_called_once_with("address", "username", "password")
565
+        mgr.get_running_config.assert_called_once_with()
566
+        self.assertFalse(mgr.configure.called)
567
+
568
+
569
+class TestCFABdriverSharedPprofilePrefixed(BaseTestCFABdriver):
570
+    """Test Fujitsu C-Fabric mechanism driver with pprofile prefix.
571
+    """
572
+
573
+    def setUp(self):
574
+        cfg.CONF.set_override('share_pprofile', True, "fujitsu_cfab")
575
+        cfg.CONF.set_override('pprofile_prefix', "test-", "fujitsu_cfab")
576
+        super(TestCFABdriverSharedPprofilePrefixed, self).setUp()
577
+
578
+    def test_associate_mac_to_network(self):
579
+        mgr = self.driver.mgr
580
+        """:type : mock.MagicMock"""
581
+        mgr.get_running_config.return_value = """pprofile test-2 vlan tag 2
582
+vfab 3 pprofile 0 vsiid mac 00:00:00:00:00:01 test-2
583
+"""
584
+
585
+        self.driver.associate_mac_to_network(
586
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
587
+
588
+        mgr.connect.assert_called_once_with("address", "username", "password")
589
+        mgr.get_running_config.assert_called_once_with()
590
+        self.assert_configured(
591
+            ["vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:05 test-2"])
592
+
593
+    def test_associate_mac_to_network_no_pprofile(self):
594
+        mgr = self.driver.mgr
595
+        """:type : mock.MagicMock"""
596
+        mgr.get_running_config.return_value = ""
597
+
598
+        self.driver.associate_mac_to_network(
599
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
600
+
601
+        mgr.connect.assert_called_once_with("address", "username", "password")
602
+        mgr.get_running_config.assert_called_once_with()
603
+        self.assert_configured(
604
+            ["pprofile test-2 vlan tag 2",
605
+             "vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 test-2"])
606
+
607
+    def test_dissociate_mac_from_network(self):
608
+        mgr = self.driver.mgr
609
+        """:type : mock.MagicMock"""
610
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
611
+pprofile test-1 vlan tag 2
612
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 test-1
613
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:06 1
614
+"""
615
+
616
+        self.driver.dissociate_mac_from_network(
617
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
618
+
619
+        mgr.connect.assert_called_once_with("address", "username", "password")
620
+        mgr.get_running_config.assert_called_once_with()
621
+        self.assert_configured(
622
+            ["no vfab 3 pprofile 0",
623
+             "no pprofile test-1"])
624
+
625
+    def test_dissociate_mac_from_network_still_used(self):
626
+        mgr = self.driver.mgr
627
+        """:type : mock.MagicMock"""
628
+        mgr.get_running_config.return_value = """pprofile test-1 vlan tag 2
629
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 test-1
630
+vfab 3 pprofile 1 vsiid mac 00:01:02:03:04:06 test-1
631
+"""
632
+
633
+        self.driver.dissociate_mac_from_network(
634
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
635
+
636
+        mgr.connect.assert_called_once_with("address", "username", "password")
637
+        mgr.get_running_config.assert_called_once_with()
638
+        self.assert_configured(["no vfab 3 pprofile 0"])
639
+
640
+    def test_dissociate_mac_from_network_still_used_in_other_vfab(self):
641
+        mgr = self.driver.mgr
642
+        """:type : mock.MagicMock"""
643
+        mgr.get_running_config.return_value = """pprofile test-1 vlan tag 2
644
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 test-1
645
+vfab 4 pprofile 0 vsiid mac 00:01:02:03:04:06 test-1
646
+"""
647
+
648
+        self.driver.dissociate_mac_from_network(
649
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
650
+
651
+        mgr.connect.assert_called_once_with("address", "username", "password")
652
+        mgr.get_running_config.assert_called_once_with()
653
+        self.assert_configured(["no vfab 3 pprofile 0"])
654
+
655
+    def test_dissociate_mac_from_network_no_match(self):
656
+        mgr = self.driver.mgr
657
+        """:type : mock.MagicMock"""
658
+        mgr.get_running_config.return_value = """pprofile 1 vlan tag 2
659
+vfab 3 pprofile 0 vsiid mac 00:01:02:03:04:05 1
660
+"""
661
+
662
+        self.driver.dissociate_mac_from_network(
663
+            "address", "username", "password", "3", 2, "00:01:02:03:04:05")
664
+
665
+        mgr.connect.assert_called_once_with("address", "username", "password")
666
+        mgr.get_running_config.assert_called_once_with()
667
+        self.assertFalse(mgr.configure.called)
668
+
669
+
670
+class TestCFABdriverPprofilePrefix(base.BaseTestCase):
671
+    """Test Fujitsu C-Fabric mechanism driver for pprofile_prefix errors.
672
+    """
673
+
674
+    def test_too_long(self):
675
+        cfg.CONF.set_override('pprofile_prefix', "a" * 29, "fujitsu_cfab")
676
+        with mock.patch(_CFABDRIVER__CFABMANAGER, autospec=True):
677
+            self.assertRaises(ValueError, cfabdriver.CFABdriver, cfg.CONF)
678
+
679
+    def test_illegal_character(self):
680
+        cfg.CONF.set_override('pprofile_prefix', '"', "fujitsu_cfab")
681
+        with mock.patch(_CFABDRIVER__CFABMANAGER, autospec=True):
682
+            self.assertRaises(ValueError, cfabdriver.CFABdriver, cfg.CONF)

+ 92
- 0
networking_fujitsu/tests/unit/ml2/drivers/fujitsu/cfab/test_fujitsu_mechanism_driver.py View File

@@ -0,0 +1,92 @@
1
+# Copyright 2015 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain 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,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+# See the License for the specific language governing permissions and
14
+# limitations under the License.
15
+import os
16
+
17
+import mock
18
+
19
+try:
20
+    from oslo_log import log as logging
21
+except ImportError:
22
+    from neutron.openstack.common import log as logging
23
+
24
+from networking_fujitsu.ml2.drivers.fujitsu.cfab\
25
+    import (mechanism_fujitsu as fujitsumechanism)
26
+from networking_fujitsu.ml2.drivers.fujitsu.cfab import cfabdriver
27
+from neutron.plugins.ml2 import config as ml2_config
28
+from neutron.tests.unit.plugins.ml2 import test_plugin as test_ml2_plugin
29
+
30
+LOG = logging.getLogger(__name__)
31
+
32
+
33
+class TestFujitsuMechDriverV2(test_ml2_plugin.Ml2PluginV2TestCase):
34
+    """Test Fujitsu mechanism driver.
35
+
36
+    If the environment variable OS_FUJITSU_CFAB_ADDRESS is defined, tests will
37
+    be performed using the C-Fabric driver, and a telnet connection to the
38
+    address OS_FUJITSU_CFAB_ADDRESS and the port OS_FUJITSU_CFAB_PORT (defaults
39
+    to 23) will be established. The username will be taken from
40
+    OS_FUJITSU_CFAB_USERNAME (defaults to "admin") and the password will be
41
+    taken from OS_FUJITSU_CFAB_PASSWORD (defaults to "password").
42
+    If the environment variable OS_FUJITSU_CFAB_ADDRESS is NOT defined, tests
43
+    will be performed using the mock driver instead of the C-Fabric driver, and
44
+    no real telnet connection will be used.
45
+    """
46
+
47
+    _mechanism_drivers = ['fujitsu_cfab']
48
+
49
+    def setUp(self):
50
+
51
+        ml2_config.cfg.CONF.set_override(
52
+            'tenant_network_types', ['vlan'], 'ml2')
53
+
54
+        address = os.environ.get('OS_FUJITSU_CFAB_ADDRESS')
55
+        if address:
56
+            ml2_fujitsu_opts = {
57
+                'username': os.environ.get('OS_FUJITSU_CFAB_USERNAME') or
58
+                "admin",
59
+                'password': os.environ.get('OS_FUJITSU_CFAB_PASSWORD') or
60
+                "password",
61
+                'address': address,
62
+                'physical_networks': ["physnet1:1", "physnet2:2"],
63
+                'pprofile_prefix': "test-"}
64
+
65
+            for opt, val in ml2_fujitsu_opts.items():
66
+                ml2_config.cfg.CONF.set_override(opt, val, "fujitsu_cfab")
67
+
68
+            try:
69
+                cfabdriver.TELNET_PORT = int(
70
+                    os.environ['OS_FUJITSU_CFAB_PORT'])
71
+            except KeyError:
72
+                pass
73
+            super(TestFujitsuMechDriverV2, self).setUp()
74
+        else:
75
+            def mocked_initialize(self):
76
+                self._switch = {'address': "", 'username': "", 'password': ""}
77
+                self._driver = mock.MagicMock()
78
+                self._physical_networks = {'physnet1': "1", 'physnet2': "2"}
79
+
80
+            with mock.patch.object(fujitsumechanism.FujitsuMechanism,
81
+                                   'initialize', new=mocked_initialize):
82
+                super(TestFujitsuMechDriverV2, self).setUp()
83
+
84
+
85
+class TestFujitsuMechDriverNetworksV2(test_ml2_plugin.TestMl2NetworksV2,
86
+                                      TestFujitsuMechDriverV2):
87
+    pass
88
+
89
+
90
+class TestFujitsuMechDriverPortsV2(test_ml2_plugin.TestMl2PortsV2,
91
+                                   TestFujitsuMechDriverV2):
92
+    pass

+ 13
- 2
setup.cfg View File

@@ -3,7 +3,7 @@ name = networking-fujitsu
3 3
 summary = Fujitsu ML2 plugins/drivers for OpenStack Neutron
4 4
 description-file =
5 5
     README.rst
6
-author = OpenStack
6
+author = Fujitsu
7 7
 author-email = openstack-dev@lists.openstack.org
8 8
 home-page = http://www.openstack.org/
9 9
 classifier =
@@ -22,6 +22,17 @@ classifier =
22 22
 [files]
23 23
 packages =
24 24
     networking_fujitsu
25
+data_files =
26
+    etc/neutron/plugins/ml2 =
27
+        etc/neutron/plugins/ml2/ml2_conf_fujitsu_cfab.ini
28
+
29
+[global]
30
+setup-hooks =
31
+    pbr.hooks.setup_hook
32
+
33
+[entry_points]
34
+neutron.ml2.mechanism_drivers =
35
+    fujitsu_cfab = networking_fujitsu.ml2.drivers.fujitsu.cfab.mechanism_fujitsu:FujitsuMechanism
25 36
 
26 37
 [build_sphinx]
27 38
 source-dir = doc/source
@@ -43,4 +54,4 @@ input_file = networking_fujitsu/locale/networking-fujitsu.pot
43 54
 [extract_messages]
44 55
 keywords = _ gettext ngettext l_ lazy_gettext
45 56
 mapping_file = babel.cfg
46
-output_file = networking_fujitsu/locale/networking-fujitsu.pot
57
+output_file = networking_fujitsu/locale/networking-fujitsu.pot

+ 1
- 1
setup.py View File

@@ -26,4 +26,4 @@ except ImportError:
26 26
 
27 27
 setuptools.setup(
28 28
     setup_requires=['pbr'],
29
-    pbr=True)
29
+    pbr=True)

+ 2
- 1
test-requirements.txt View File

@@ -6,10 +6,11 @@ hacking<0.11,>=0.10.0
6 6
 
7 7
 coverage>=3.6
8 8
 discover
9
+webtest
9 10
 python-subunit>=0.0.18
10 11
 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
11 12
 oslosphinx>=2.5.0 # Apache-2.0
12 13
 oslotest>=1.10.0 # Apache-2.0
13 14
 testrepository>=0.0.18
14 15
 testscenarios>=0.4
15
-testtools>=1.4.0
16
+testtools>=1.4.0

+ 6
- 0
tools/pretty_tox.sh View File

@@ -0,0 +1,6 @@
1
+#! /bin/sh
2
+
3
+TESTRARGS=$1
4
+
5
+exec 3>&1
6
+status=$(exec 4>&1 >&3; (python setup.py testr --slowest --testr-args="--subunit $TESTRARGS"; echo $? >&4 ) | $(dirname $0)/subunit-trace.py -f) && exit $status

+ 307
- 0
tools/subunit-trace.py View File

@@ -0,0 +1,307 @@
1
+#!/usr/bin/env python
2
+
3
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
4
+# Copyright 2014 Samsung Electronics
5
+# All Rights Reserved.
6
+#
7
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+# not use this file except in compliance with the License. You may obtain
9
+# a copy of the License at
10
+#
11
+#     http://www.apache.org/licenses/LICENSE-2.0
12
+#
13
+# Unless required by applicable law or agreed to in writing, software
14
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+# License for the specific language governing permissions and limitations
17
+# under the License.
18
+
19
+"""Trace a subunit stream in reasonable detail and high accuracy."""
20
+
21
+import argparse
22
+import functools
23
+import os
24
+import re
25
+import sys
26
+
27
+import mimeparse
28
+import subunit
29
+import testtools
30
+
31
+DAY_SECONDS = 60 * 60 * 24
32
+FAILS = []
33
+RESULTS = {}
34
+
35
+
36
+class Starts(testtools.StreamResult):
37
+
38
+    def __init__(self, output):
39
+        super(Starts, self).__init__()
40
+        self._output = output
41
+
42
+    def startTestRun(self):
43
+        self._neednewline = False
44
+        self._emitted = set()
45
+
46
+    def status(self, test_id=None, test_status=None, test_tags=None,
47
+               runnable=True, file_name=None, file_bytes=None, eof=False,
48
+               mime_type=None, route_code=None, timestamp=None):
49
+        super(Starts, self).status(
50
+            test_id, test_status,
51
+            test_tags=test_tags, runnable=runnable, file_name=file_name,
52
+            file_bytes=file_bytes, eof=eof, mime_type=mime_type,
53
+            route_code=route_code, timestamp=timestamp)
54
+        if not test_id:
55
+            if not file_bytes:
56
+                return
57
+            if not mime_type or mime_type == 'test/plain;charset=utf8':
58
+                mime_type = 'text/plain; charset=utf-8'
59
+            primary, sub, parameters = mimeparse.parse_mime_type(mime_type)
60
+            content_type = testtools.content_type.ContentType(
61
+                primary, sub, parameters)
62
+            content = testtools.content.Content(
63
+                content_type, lambda: [file_bytes])
64
+            text = content.as_text()
65
+            if text and text[-1] not in '\r\n':
66
+                self._neednewline = True
67
+            self._output.write(text)
68
+        elif test_status == 'inprogress' and test_id not in self._emitted:
69
+            if self._neednewline:
70
+                self._neednewline = False
71
+                self._output.write('\n')
72
+            worker = ''
73
+            for tag in test_tags or ():
74
+                if tag.startswith('worker-'):
75
+                    worker = '(' + tag[7:] + ') '
76
+            if timestamp:
77
+                timestr = timestamp.isoformat()
78
+            else:
79
+                timestr = ''
80
+                self._output.write('%s: %s%s [start]\n' %
81
+                                   (timestr, worker, test_id))
82
+            self._emitted.add(test_id)
83
+
84
+
85
+def cleanup_test_name(name, strip_tags=True, strip_scenarios=False):
86
+    """Clean up the test name for display.
87
+
88
+    By default we strip out the tags in the test because they don't help us
89
+    in identifying the test that is run to it's result.
90
+
91
+    Make it possible to strip out the testscenarios information (not to
92
+    be confused with tempest scenarios) however that's often needed to
93
+    indentify generated negative tests.
94
+    """
95
+    if strip_tags:
96
+        tags_start = name.find('[')
97
+        tags_end = name.find(']')
98
+        if tags_start > 0 and tags_end > tags_start:
99
+            newname = name[:tags_start]
100
+            newname += name[tags_end + 1:]
101
+            name = newname
102
+
103
+    if strip_scenarios:
104
+        tags_start = name.find('(')
105
+        tags_end = name.find(')')
106
+        if tags_start > 0 and tags_end > tags_start:
107
+            newname = name[:tags_start]
108
+            newname += name[tags_end + 1:]
109
+            name = newname
110
+
111
+    return name
112
+
113
+
114
+def get_duration(timestamps):
115
+    start, end = timestamps
116
+    if not start or not end:
117
+        duration = ''
118
+    else:
119
+        delta = end - start
120
+        duration = '%d.%06ds' % (
121
+            delta.days * DAY_SECONDS + delta.seconds, delta.microseconds)
122
+    return duration
123
+
124
+
125
+def find_worker(test):
126
+    for tag in test['tags']:
127
+        if tag.startswith('worker-'):
128
+            return int(tag[7:])
129
+    return 'NaN'
130
+
131
+
132
+# Print out stdout/stderr if it exists, always
133
+def print_attachments(stream, test, all_channels=False):
134
+    """Print out subunit attachments.
135
+
136
+    Print out subunit attachments that contain content. This
137
+    runs in 2 modes, one for successes where we print out just stdout
138
+    and stderr, and an override that dumps all the attachments.
139
+    """
140
+    channels = ('stdout', 'stderr')
141
+    for name, detail in test['details'].items():
142
+        # NOTE(sdague): the subunit names are a little crazy, and actually
143
+        # are in the form pythonlogging:'' (with the colon and quotes)
144
+        name = name.split(':')[0]
145
+        if detail.content_type.type == 'test':
146
+            detail.content_type.type = 'text'
147
+        if (all_channels or name in channels) and detail.as_text():
148
+            title = "Captured %s:" % name
149
+            stream.write("\n%s\n%s\n" % (title, ('~' * len(title))))
150
+            # indent attachment lines 4 spaces to make them visually
151
+            # offset
152
+            for line in detail.as_text().split('\n'):
153
+                stream.write("    %s\n" % line)
154
+
155
+
156
+def show_outcome(stream, test, print_failures=False, failonly=False):
157
+    global RESULTS
158
+    status = test['status']
159
+    # TODO(sdague): ask lifeless why on this?
160
+    if status == 'exists':
161
+        return
162
+
163
+    worker = find_worker(test)
164
+    name = cleanup_test_name(test['id'])
165
+    duration = get_duration(test['timestamps'])
166
+
167
+    if worker not in RESULTS:
168
+        RESULTS[worker] = []
169
+    RESULTS[worker].append(test)
170
+
171
+    # don't count the end of the return code as a fail
172
+    if name == 'process-returncode':
173
+        return
174
+
175
+    if status == 'fail':
176
+        FAILS.append(test)
177
+        stream.write('{%s} %s [%s] ... FAILED\n' % (
178
+            worker, name, duration))
179
+        if not print_failures:
180
+            print_attachments(stream, test, all_channels=True)
181
+    elif not failonly:
182
+        if status == 'success':
183
+            stream.write('{%s} %s [%s] ... ok\n' % (
184
+                worker, name, duration))
185
+            print_attachments(stream, test)
186
+        elif status == 'skip':
187
+            stream.write('{%s} %s ... SKIPPED: %s\n' % (
188
+                worker, name, test['details']['reason'].as_text()))
189
+        else:
190
+            stream.write('{%s} %s [%s] ... %s\n' % (
191
+                worker, name, duration, test['status']))
192
+            if not print_failures:
193
+                print_attachments(stream, test, all_channels=True)
194
+
195
+    stream.flush()
196
+
197
+
198
+def print_fails(stream):
199
+    """Print summary failure report.
200
+
201
+    Currently unused, however there remains debate on inline vs. at end
202
+    reporting, so leave the utility function for later use.
203
+    """
204
+    if not FAILS:
205
+        return
206
+    stream.write("\n==============================\n")
207
+    stream.write("Failed %s tests - output below:" % len(FAILS))
208
+    stream.write("\n==============================\n")
209
+    for f in FAILS:
210
+        stream.write("\n%s\n" % f['id'])
211
+        stream.write("%s\n" % ('-' * len(f['id'])))
212
+        print_attachments(stream, f, all_channels=True)
213
+    stream.write('\n')
214
+
215
+
216
+def count_tests(key, value):
217
+    count = 0
218
+    for k, v in RESULTS.items():
219
+        for item in v:
220
+            if key in item:
221
+                if re.search(value, item[key]):
222
+                    count += 1
223
+    return count
224
+
225
+
226
+def run_time():
227
+    runtime = 0.0
228
+    for k, v in RESULTS.items():
229
+        for test in v:
230
+            runtime += float(get_duration(test['timestamps']).strip('s'))
231
+    return runtime
232
+
233
+
234
+def worker_stats(worker):
235
+    tests = RESULTS[worker]
236
+    num_tests = len(tests)
237
+    delta = tests[-1]['timestamps'][1] - tests[0]['timestamps'][0]
238
+    return num_tests, delta
239
+
240
+
241
+def print_summary(stream):
242
+    stream.write("\n======\nTotals\n======\n")
243
+    stream.write("Run: %s in %s sec.\n" % (count_tests('status', '.*'),
244
+                                           run_time()))
245
+    stream.write(" - Passed: %s\n" % count_tests('status', 'success'))
246
+    stream.write(" - Skipped: %s\n" % count_tests('status', 'skip'))
247
+    stream.write(" - Failed: %s\n" % count_tests('status', 'fail'))
248
+
249
+    # we could have no results, especially as we filter out the process-codes
250
+    if RESULTS:
251
+        stream.write("\n==============\nWorker Balance\n==============\n")
252
+
253
+        for w in range(max(RESULTS.keys()) + 1):
254
+            if w not in RESULTS:
255
+                stream.write(
256
+                    " - WARNING: missing Worker %s! "
257
+                    "Race in testr accounting.\n" % w)
258
+            else:
259
+                num, time = worker_stats(w)
260
+                stream.write(" - Worker %s (%s tests) => %ss\n" %
261
+                             (w, num, time))
262
+
263
+
264
+def parse_args():
265
+    parser = argparse.ArgumentParser()
266
+    parser.add_argument('--no-failure-debug', '-n', action='store_true',
267
+                        dest='print_failures', help='Disable printing failure '
268
+                        'debug information in realtime')
269
+    parser.add_argument('--fails', '-f', action='store_true',
270
+                        dest='post_fails', help='Print failure debug '
271
+                        'information after the stream is proccesed')
272
+    parser.add_argument('--failonly', action='store_true',
273
+                        dest='failonly', help="Don't print success items",
274
+                        default=(
275
+                            os.environ.get('TRACE_FAILONLY', False)
276
+                            is not False))
277
+    return parser.parse_args()
278
+
279
+
280
+def main():
281
+    args = parse_args()
282
+    stream = subunit.ByteStreamToStreamResult(
283
+        sys.stdin, non_subunit_name='stdout')
284
+    starts = Starts(sys.stdout)
285
+    outcomes = testtools.StreamToDict(
286
+        functools.partial(show_outcome, sys.stdout,
287
+                          print_failures=args.print_failures,
288
+                          failonly=args.failonly
289
+                          ))
290
+    summary = testtools.StreamSummary()
291
+    result = testtools.CopyStreamResult([starts, outcomes, summary])
292
+    result.startTestRun()
293
+    try:
294
+        stream.run(result)
295
+    finally:
296
+        result.stopTestRun()
297
+    if count_tests('status', '.*') == 0:
298
+        print("The test run didn't actually run any tests")
299
+        return 1
300
+    if args.post_fails:
301
+        print_fails(sys.stdout)
302
+    print_summary(sys.stdout)
303
+    return (0 if summary.wasSuccessful() else 1)
304
+
305
+
306
+if __name__ == '__main__':
307
+    sys.exit(main())

+ 15
- 6
tox.ini View File

@@ -1,6 +1,6 @@
1 1
 [tox]
2 2
 minversion = 1.6
3
-envlist = py34,py27,pypy,pep8
3
+envlist = py27,pep8
4 4
 skipsdist = True
5 5
 
6 6
 [testenv]
@@ -8,9 +8,12 @@ usedevelop = True
8 8
 install_command = pip install -U {opts} {packages}
9 9
 setenv =
10 10
    VIRTUAL_ENV={envdir}
11
-deps = -r{toxinidir}/test-requirements.txt
12
-commands = python setup.py test --slowest --testr-args='{posargs}'
11
+deps = -egit+https://git.openstack.org/openstack/neutron#egg=neutron
12
+       -r{toxinidir}/requirements.txt
13
+       -r{toxinidir}/test-requirements.txt
13 14
 
15
+whitelist_externals = sh
16
+commands = sh tools/pretty_tox.sh '{posargs}'
14 17
 [testenv:pep8]
15 18
 commands = flake8
16 19
 
@@ -28,8 +31,14 @@ commands = oslo_debug_helper {posargs}
28 31
 
29 32
 [flake8]
30 33
 # E123, E125 skipped as they are invalid PEP-8.
31
-
34
+# E125 continuation line does not distinguish itself from next logical line
35
+# E126 continuation line over-indented for hanging indent
36
+# E128 continuation line under-indented for visual indent
37
+# E129 visually indented line with same indent as next logical line
38
+# E265 block comment should start with ‘#
39
+# H305 imports not grouped correctly
40
+# H405 multi line docstring summary not separated with an empty line
32 41
 show-source = True
33
-ignore = E123,E125
42
+ignore = E123,E125,E126,E128,E129,E265,H305,H405
34 43
 builtins = _
35
-exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
44
+exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build

Loading…
Cancel
Save