Merge lp:~james-page/charm-helpers/ovs-helper into lp:charm-helpers

Proposed by James Page
Status: Merged
Merged at revision: 39
Proposed branch: lp:~james-page/charm-helpers/ovs-helper
Merge into: lp:charm-helpers
Diff against target: 288 lines (+274/-0)
2 files modified
charmhelpers/contrib/network/ovs/__init__.py (+72/-0)
tests/contrib/network/test_ovs.py (+202/-0)
To merge this branch: bzr merge lp:~james-page/charm-helpers/ovs-helper
Reviewer Review Type Date Requested Status
Adam Gandelman (community) Approve
Review via email: mp+172296@code.launchpad.net

Description of the change

Various helpers for interacting with OpenvSwitch.

To post a comment you must log in.
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Simple utility wrappers for OVS CLI commands with good tests. LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'charmhelpers/contrib/network'
2=== added file 'charmhelpers/contrib/network/__init__.py'
3=== added directory 'charmhelpers/contrib/network/ovs'
4=== added file 'charmhelpers/contrib/network/ovs/__init__.py'
5--- charmhelpers/contrib/network/ovs/__init__.py 1970-01-01 00:00:00 +0000
6+++ charmhelpers/contrib/network/ovs/__init__.py 2013-07-01 10:37:46 +0000
7@@ -0,0 +1,72 @@
8+''' Helpers for interacting with OpenvSwitch '''
9+import subprocess
10+import os
11+from charmhelpers.core.hookenv import (
12+ log, WARNING
13+)
14+from charmhelpers.core.host import (
15+ service
16+)
17+
18+
19+def add_bridge(name):
20+ ''' Add the named bridge to openvswitch '''
21+ log('Creating bridge {}'.format(name))
22+ subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name])
23+
24+
25+def del_bridge(name):
26+ ''' Delete the named bridge from openvswitch '''
27+ log('Deleting bridge {}'.format(name))
28+ subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name])
29+
30+
31+def add_bridge_port(name, port):
32+ ''' Add a port to the named openvswitch bridge '''
33+ log('Adding port {} to bridge {}'.format(port, name))
34+ subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port",
35+ name, port])
36+ subprocess.check_call(["ip", "link", "set", port, "up"])
37+
38+
39+def del_bridge_port(name, port):
40+ ''' Delete a port from the named openvswitch bridge '''
41+ log('Deleting port {} from bridge {}'.format(port, name))
42+ subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port",
43+ name, port])
44+ subprocess.check_call(["ip", "link", "set", port, "down"])
45+
46+
47+def set_manager(manager):
48+ ''' Set the controller for the local openvswitch '''
49+ log('Setting manager for local ovs to {}'.format(manager))
50+ subprocess.check_call(['ovs-vsctl', 'set-manager',
51+ 'ssl:{}'.format(manager)])
52+
53+
54+CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem'
55+
56+
57+def get_certificate():
58+ ''' Read openvswitch certificate from disk '''
59+ if os.path.exists(CERT_PATH):
60+ log('Reading ovs certificate from {}'.format(CERT_PATH))
61+ with open(CERT_PATH, 'r') as cert:
62+ full_cert = cert.read()
63+ begin_marker = "-----BEGIN CERTIFICATE-----"
64+ end_marker = "-----END CERTIFICATE-----"
65+ begin_index = full_cert.find(begin_marker)
66+ end_index = full_cert.rfind(end_marker)
67+ if end_index == -1 or begin_index == -1:
68+ raise RuntimeError("Certificate does not contain valid begin"
69+ " and end markers.")
70+ full_cert = full_cert[begin_index:(end_index + len(end_marker))]
71+ return full_cert
72+ else:
73+ log('Certificate not found', level=WARNING)
74+ return None
75+
76+
77+def full_restart():
78+ ''' Full restart and reload of openvswitch '''
79+ service('force-reload-kmod', 'openvswitch-switch')
80
81=== added directory 'tests/contrib/network'
82=== added file 'tests/contrib/network/__init__.py'
83=== added file 'tests/contrib/network/test_ovs.py'
84--- tests/contrib/network/test_ovs.py 1970-01-01 00:00:00 +0000
85+++ tests/contrib/network/test_ovs.py 2013-07-01 10:37:46 +0000
86@@ -0,0 +1,202 @@
87+from contextlib import contextmanager
88+from mock import patch, call, MagicMock
89+from testtools import TestCase
90+
91+import charmhelpers.contrib.network.ovs as ovs
92+
93+
94+@contextmanager
95+def patch_open():
96+ '''Patch open() to allow mocking both open() itself and the file that is
97+ yielded.
98+
99+ Yields the mock for "open" and "file", respectively.'''
100+ mock_open = MagicMock(spec=open)
101+ mock_file = MagicMock(spec=file)
102+
103+ @contextmanager
104+ def stub_open(*args, **kwargs):
105+ mock_open(*args, **kwargs)
106+ yield mock_file
107+
108+ with patch('__builtin__.open', stub_open):
109+ yield mock_open, mock_file
110+
111+GOOD_CERT = '''Certificate:
112+ Data:
113+ Version: 1 (0x0)
114+ Serial Number: 13798680962510501282 (0xbf7ec33a136235a2)
115+ Signature Algorithm: sha1WithRSAEncryption
116+ Issuer: C=US, ST=CA, L=Palo Alto, O=Open vSwitch, OU=Open vSwitch
117+ Validity
118+ Not Before: Jun 28 17:02:19 2013 GMT
119+ Not After : Jun 28 17:02:19 2019 GMT
120+ Subject: C=US, ST=CA, L=Palo Alto, O=Open vSwitch, OU=Open vSwitch
121+ Subject Public Key Info:
122+ Public Key Algorithm: rsaEncryption
123+ Public-Key: (2048 bit)
124+ Modulus:
125+ 00:e8:a7:db:0a:6d:c0:16:4a:14:96:1d:74:91:15:
126+ 64:3f:ae:2a:54:be:2a:fe:10:14:9a:73:39:d8:58:
127+ 74:7f:ab:d5:f2:39:aa:9a:27:7c:31:82:f8:74:42:
128+ 46:8d:c5:3b:42:55:52:be:75:7f:a5:b1:ec:d5:29:
129+ 9f:62:0e:de:31:27:2b:95:1f:24:0d:ca:8c:48:30:
130+ 96:9f:ba:b7:9d:eb:c1:bd:93:05:e3:d8:ca:66:5a:
131+ e9:cb:a5:7a:3a:8d:27:e2:05:9d:88:fc:a9:ef:af:
132+ 47:4c:66:ce:c6:43:73:1a:85:f4:5f:b9:53:5b:29:
133+ f3:c3:23:1f:0c:20:95:11:50:71:b2:f6:01:23:3f:
134+ 66:0f:5c:43:c2:90:fb:e5:98:73:98:e9:38:bb:1f:
135+ 1b:89:97:1e:dc:d7:98:07:68:32:ec:da:1d:69:0b:
136+ e2:df:40:fb:64:52:e5:e9:40:27:b0:ca:73:21:51:
137+ f6:8f:00:20:c0:2b:1a:d4:01:c2:32:38:9d:d1:8d:
138+ 88:71:46:a9:42:0d:ee:3b:1c:88:db:27:69:49:f9:
139+ 60:34:70:61:3d:60:df:7e:e4:e1:1d:c6:16:89:05:
140+ ba:31:06:eb:88:b5:78:94:5d:8c:9d:88:fe:f2:c2:
141+ 80:a1:04:15:d3:84:85:d3:aa:5a:1d:53:5c:f8:57:
142+ ae:61
143+ Exponent: 65537 (0x10001)
144+ Signature Algorithm: sha1WithRSAEncryption
145+ 14:7e:ca:c3:fc:93:60:9f:80:e0:65:2e:ef:41:2d:f9:af:77:
146+ da:6d:e2:e0:11:70:17:fb:e5:67:4c:f0:ad:39:ec:96:ef:fe:
147+ d5:95:94:70:e5:52:31:68:63:8c:ea:b3:a1:8e:02:e2:91:4b:
148+ a8:8c:07:86:fd:80:98:a2:b1:90:2b:9c:2e:ab:f4:73:9d:8f:
149+ fd:31:b9:8f:fe:6c:af:d6:bf:72:44:89:08:93:19:ef:2b:c3:
150+ 7c:ab:ba:bc:57:ca:f1:17:e4:e8:81:40:ca:65:df:84:be:10:
151+ 2c:42:46:af:d2:e0:0d:df:5d:56:53:65:13:e0:20:55:b4:ee:
152+ cd:5e:b5:c4:97:1d:3e:a6:c1:9c:7e:b8:87:ee:64:78:a5:59:
153+ e5:b2:79:47:9a:8e:59:fa:c4:18:ea:27:fd:a2:d5:76:d0:ae:
154+ d9:05:f6:0e:23:ca:7d:66:a1:ba:18:67:f5:6d:bb:51:5a:f5:
155+ 52:e9:17:bb:63:15:24:b4:61:25:9f:d9:9c:89:58:93:9a:c3:
156+ 74:55:72:3e:f9:ff:ef:54:7d:e8:28:78:ba:3c:c7:15:ba:b9:
157+ c6:e3:8c:61:cb:a9:ed:8d:07:16:0d:8d:f6:1c:36:11:69:08:
158+ b8:45:7d:fc:fd:d1:ab:2d:9b:4e:9c:dd:11:78:50:c7:87:9f:
159+ 4a:24:9c:a0
160+-----BEGIN CERTIFICATE-----
161+MIIDwjCCAqoCCQC/fsM6E2I1ojANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
162+VVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDE9w
163+ZW4gdlN3aXRjaDEfMB0GA1UECxMWT3BlbiB2U3dpdGNoIGNlcnRpZmllcjE6MDgG
164+A1UEAxMxb3ZzY2xpZW50IGlkOjU4MTQ5N2E1LWJjMDAtNGVjYy1iNzkwLTU3NTZj
165+ZWUxNmE0ODAeFw0xMzA2MjgxNzAyMTlaFw0xOTA2MjgxNzAyMTlaMIGiMQswCQYD
166+VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UE
167+ChMMT3BlbiB2U3dpdGNoMR8wHQYDVQQLExZPcGVuIHZTd2l0Y2ggY2VydGlmaWVy
168+MTowOAYDVQQDEzFvdnNjbGllbnQgaWQ6NTgxNDk3YTUtYmMwMC00ZWNjLWI3OTAt
169+NTc1NmNlZTE2YTQ4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Kfb
170+Cm3AFkoUlh10kRVkP64qVL4q/hAUmnM52Fh0f6vV8jmqmid8MYL4dEJGjcU7QlVS
171+vnV/pbHs1SmfYg7eMScrlR8kDcqMSDCWn7q3nevBvZMF49jKZlrpy6V6Oo0n4gWd
172+iPyp769HTGbOxkNzGoX0X7lTWynzwyMfDCCVEVBxsvYBIz9mD1xDwpD75ZhzmOk4
173+ux8biZce3NeYB2gy7NodaQvi30D7ZFLl6UAnsMpzIVH2jwAgwCsa1AHCMjid0Y2I
174+cUapQg3uOxyI2ydpSflgNHBhPWDffuThHcYWiQW6MQbriLV4lF2MnYj+8sKAoQQV
175+04SF06paHVNc+FeuYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAUfsrD/JNgn4Dg
176+ZS7vQS35r3fabeLgEXAX++VnTPCtOeyW7/7VlZRw5VIxaGOM6rOhjgLikUuojAeG
177+/YCYorGQK5wuq/RznY/9MbmP/myv1r9yRIkIkxnvK8N8q7q8V8rxF+TogUDKZd+E
178+vhAsQkav0uAN311WU2UT4CBVtO7NXrXElx0+psGcfriH7mR4pVnlsnlHmo5Z+sQY
179+6if9otV20K7ZBfYOI8p9ZqG6GGf1bbtRWvVS6Re7YxUktGEln9mciViTmsN0VXI+
180++f/vVH3oKHi6PMcVurnG44xhy6ntjQcWDY32HDYRaQi4RX38/dGrLZtOnN0ReFDH
181+h59KJJyg
182+-----END CERTIFICATE-----
183+'''
184+
185+PEM_ENCODED = '''-----BEGIN CERTIFICATE-----
186+MIIDwjCCAqoCCQC/fsM6E2I1ojANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
187+VVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDE9w
188+ZW4gdlN3aXRjaDEfMB0GA1UECxMWT3BlbiB2U3dpdGNoIGNlcnRpZmllcjE6MDgG
189+A1UEAxMxb3ZzY2xpZW50IGlkOjU4MTQ5N2E1LWJjMDAtNGVjYy1iNzkwLTU3NTZj
190+ZWUxNmE0ODAeFw0xMzA2MjgxNzAyMTlaFw0xOTA2MjgxNzAyMTlaMIGiMQswCQYD
191+VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UE
192+ChMMT3BlbiB2U3dpdGNoMR8wHQYDVQQLExZPcGVuIHZTd2l0Y2ggY2VydGlmaWVy
193+MTowOAYDVQQDEzFvdnNjbGllbnQgaWQ6NTgxNDk3YTUtYmMwMC00ZWNjLWI3OTAt
194+NTc1NmNlZTE2YTQ4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Kfb
195+Cm3AFkoUlh10kRVkP64qVL4q/hAUmnM52Fh0f6vV8jmqmid8MYL4dEJGjcU7QlVS
196+vnV/pbHs1SmfYg7eMScrlR8kDcqMSDCWn7q3nevBvZMF49jKZlrpy6V6Oo0n4gWd
197+iPyp769HTGbOxkNzGoX0X7lTWynzwyMfDCCVEVBxsvYBIz9mD1xDwpD75ZhzmOk4
198+ux8biZce3NeYB2gy7NodaQvi30D7ZFLl6UAnsMpzIVH2jwAgwCsa1AHCMjid0Y2I
199+cUapQg3uOxyI2ydpSflgNHBhPWDffuThHcYWiQW6MQbriLV4lF2MnYj+8sKAoQQV
200+04SF06paHVNc+FeuYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAUfsrD/JNgn4Dg
201+ZS7vQS35r3fabeLgEXAX++VnTPCtOeyW7/7VlZRw5VIxaGOM6rOhjgLikUuojAeG
202+/YCYorGQK5wuq/RznY/9MbmP/myv1r9yRIkIkxnvK8N8q7q8V8rxF+TogUDKZd+E
203+vhAsQkav0uAN311WU2UT4CBVtO7NXrXElx0+psGcfriH7mR4pVnlsnlHmo5Z+sQY
204+6if9otV20K7ZBfYOI8p9ZqG6GGf1bbtRWvVS6Re7YxUktGEln9mciViTmsN0VXI+
205++f/vVH3oKHi6PMcVurnG44xhy6ntjQcWDY32HDYRaQi4RX38/dGrLZtOnN0ReFDH
206+h59KJJyg
207+-----END CERTIFICATE-----'''
208+
209+BAD_CERT = ''' NO MARKERS '''
210+
211+
212+class OVSHelpersTest(TestCase):
213+
214+ @patch.object(ovs, 'log')
215+ @patch('subprocess.check_call')
216+ def test_add_bridge(self, check_call, log):
217+ ovs.add_bridge('test')
218+ check_call.assert_called_with(["ovs-vsctl", "--", "--may-exist",
219+ "add-br", 'test'])
220+ self.assertTrue(log.call_count == 1)
221+
222+ @patch.object(ovs, 'log')
223+ @patch('subprocess.check_call')
224+ def test_del_bridge(self, check_call, log):
225+ ovs.del_bridge('test')
226+ check_call.assert_called_with(["ovs-vsctl", "--", "--if-exists",
227+ "del-br", 'test'])
228+ self.assertTrue(log.call_count == 1)
229+
230+ @patch.object(ovs, 'log')
231+ @patch('subprocess.check_call')
232+ def test_add_bridge_port(self, check_call, log):
233+ ovs.add_bridge_port('test', 'eth1')
234+ check_call.assert_has_calls([
235+ call(["ovs-vsctl", "--", "--may-exist", "add-port",
236+ 'test', 'eth1']),
237+ call(['ip', 'link', 'set', 'eth1', 'up'])
238+ ])
239+ self.assertTrue(log.call_count == 1)
240+
241+ @patch.object(ovs, 'log')
242+ @patch('subprocess.check_call')
243+ def test_del_bridge_port(self, check_call, log):
244+ ovs.del_bridge_port('test', 'eth1')
245+ check_call.assert_has_calls([
246+ call(["ovs-vsctl", "--", "--if-exists", "del-port",
247+ 'test', 'eth1']),
248+ call(['ip', 'link', 'set', 'eth1', 'down'])
249+ ])
250+ self.assertTrue(log.call_count == 1)
251+
252+ @patch.object(ovs, 'log')
253+ @patch('subprocess.check_call')
254+ def test_set_manager(self, check_call, log):
255+ ovs.set_manager('manager')
256+ check_call.assert_called_with(['ovs-vsctl', 'set-manager',
257+ 'ssl:manager'])
258+ self.assertTrue(log.call_count == 1)
259+
260+ @patch.object(ovs, 'log')
261+ @patch('os.path.exists')
262+ def test_get_certificate_good_cert(self, exists, log):
263+ exists.return_value = True
264+ with patch_open() as (mock_open, mock_file):
265+ mock_file.read.return_value = GOOD_CERT
266+ self.assertEqual(ovs.get_certificate(), PEM_ENCODED)
267+ self.assertTrue(log.call_count == 1)
268+
269+ @patch.object(ovs, 'log')
270+ @patch('os.path.exists')
271+ def test_get_certificate_bad_cert(self, exists, log):
272+ exists.return_value = True
273+ with patch_open() as (mock_open, mock_file):
274+ mock_file.read.return_value = BAD_CERT
275+ self.assertRaises(RuntimeError, ovs.get_certificate)
276+ self.assertTrue(log.call_count == 1)
277+
278+ @patch.object(ovs, 'log')
279+ @patch('os.path.exists')
280+ def test_get_certificate_missing(self, exists, log):
281+ exists.return_value = False
282+ self.assertIsNone(ovs.get_certificate())
283+ self.assertTrue(log.call_count == 1)
284+
285+ @patch.object(ovs, 'service')
286+ def test_full_restart(self, service):
287+ ovs.full_restart()
288+ service.assert_called_with('force-reload-kmod', 'openvswitch-switch')

Subscribers

People subscribed via source and target branches