]>
Commit | Line | Data |
---|---|---|
a3acf0b0 | 1 | #!/usr/bin/python |
e0edde6f | 2 | # Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc. |
a3acf0b0 JP |
3 | # |
4 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | # you may not use this file except in compliance with the License. | |
6 | # You may obtain a copy of the License at: | |
7 | # | |
8 | # http://www.apache.org/licenses/LICENSE-2.0 | |
9 | # | |
10 | # Unless required by applicable law or agreed to in writing, software | |
11 | # distributed under the License is distributed on an "AS IS" BASIS, | |
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | # See the License for the specific language governing permissions and | |
14 | # limitations under the License. | |
15 | ||
16 | ||
17 | # A daemon to monitor attempts to create GRE-over-IPsec tunnels. | |
18 | # Uses racoon and setkey to support the configuration. Assumes that | |
19 | # OVS has complete control over IPsec configuration for the box. | |
20 | ||
21 | # xxx To-do: | |
22 | # - Doesn't actually check that Interface is connected to bridge | |
3c52fa7b JP |
23 | # - If a certificate is badly formed, Racoon will refuse to start. We |
24 | # should do a better job of verifying certificates are valid before | |
25 | # adding an interface to racoon.conf. | |
a3acf0b0 JP |
26 | |
27 | ||
b153e667 | 28 | import argparse |
3c52fa7b | 29 | import glob |
a3acf0b0 | 30 | import os |
a3acf0b0 JP |
31 | import subprocess |
32 | import sys | |
33 | ||
8cdf0349 | 34 | import ovs.dirs |
a3acf0b0 JP |
35 | from ovs.db import error |
36 | from ovs.db import types | |
37 | import ovs.util | |
38 | import ovs.daemon | |
39 | import ovs.db.idl | |
7b2d10c5 | 40 | import ovs.unixctl |
53cf9963 | 41 | import ovs.unixctl.server |
27ae98ba | 42 | import ovs.vlog |
a3acf0b0 | 43 | |
27ae98ba | 44 | vlog = ovs.vlog.Vlog("ovs-monitor-ipsec") |
b54bdbe9 | 45 | root_prefix = '' # Prefix for absolute file names, for testing. |
38aad449 | 46 | SETKEY = "/usr/sbin/setkey" |
7b2d10c5 EJ |
47 | exiting = False |
48 | ||
49 | ||
50 | def unixctl_exit(conn, unused_argv, unused_aux): | |
51 | global exiting | |
52 | exiting = True | |
53 | conn.reply(None) | |
a3acf0b0 | 54 | |
0f4d9dce | 55 | |
a3acf0b0 JP |
56 | # Class to configure the racoon daemon, which handles IKE negotiation |
57 | class Racoon: | |
58 | # Default locations for files | |
59 | conf_file = "/etc/racoon/racoon.conf" | |
3c52fa7b | 60 | cert_dir = "/etc/racoon/certs" |
a3acf0b0 JP |
61 | psk_file = "/etc/racoon/psk.txt" |
62 | ||
3c52fa7b JP |
63 | # Racoon configuration header we use for IKE |
64 | conf_header = """# Configuration file generated by Open vSwitch | |
a3acf0b0 JP |
65 | # |
66 | # Do not modify by hand! | |
67 | ||
3c52fa7b JP |
68 | path pre_shared_key "%s"; |
69 | path certificate "%s"; | |
a3acf0b0 | 70 | |
3c52fa7b JP |
71 | """ |
72 | ||
73 | # Racoon configuration footer we use for IKE | |
74 | conf_footer = """sainfo anonymous { | |
75 | pfs_group 2; | |
76 | lifetime time 1 hour; | |
77 | encryption_algorithm aes; | |
78 | authentication_algorithm hmac_sha1, hmac_md5; | |
79 | compression_algorithm deflate; | |
80 | } | |
81 | ||
82 | """ | |
83 | ||
84 | # Certificate entry template. | |
85 | cert_entry = """remote %s { | |
a3acf0b0 | 86 | exchange_mode main; |
e97a1034 | 87 | nat_traversal on; |
73976ebd | 88 | ike_frag on; |
3c52fa7b JP |
89 | certificate_type x509 "%s" "%s"; |
90 | my_identifier asn1dn; | |
91 | peers_identifier asn1dn; | |
92 | peers_certfile x509 "%s"; | |
93 | verify_identifier on; | |
a3acf0b0 JP |
94 | proposal { |
95 | encryption_algorithm aes; | |
96 | hash_algorithm sha1; | |
3c52fa7b | 97 | authentication_method rsasig; |
a3acf0b0 JP |
98 | dh_group 2; |
99 | } | |
100 | } | |
101 | ||
3c52fa7b JP |
102 | """ |
103 | ||
104 | # Pre-shared key template. | |
105 | psk_entry = """remote %s { | |
106 | exchange_mode main; | |
107 | nat_traversal on; | |
108 | proposal { | |
109 | encryption_algorithm aes; | |
110 | hash_algorithm sha1; | |
111 | authentication_method pre_shared_key; | |
112 | dh_group 2; | |
113 | } | |
a3acf0b0 | 114 | } |
3c52fa7b | 115 | |
a3acf0b0 JP |
116 | """ |
117 | ||
118 | def __init__(self): | |
119 | self.psk_hosts = {} | |
120 | self.cert_hosts = {} | |
121 | ||
b54bdbe9 | 122 | if not os.path.isdir(root_prefix + self.cert_dir): |
ef035eef JP |
123 | os.mkdir(self.cert_dir) |
124 | ||
3c52fa7b | 125 | # Clean out stale peer certs from previous runs |
b54bdbe9 BP |
126 | for ovs_cert in glob.glob("%s%s/ovs-*.pem" |
127 | % (root_prefix, self.cert_dir)): | |
3c52fa7b JP |
128 | try: |
129 | os.remove(ovs_cert) | |
130 | except OSError: | |
a251af0a | 131 | vlog.warn("couldn't remove %s" % ovs_cert) |
a3acf0b0 | 132 | |
3c52fa7b JP |
133 | # Replace racoon's conf file with our template |
134 | self.commit() | |
a3acf0b0 JP |
135 | |
136 | def reload(self): | |
b54bdbe9 BP |
137 | exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon", |
138 | "reload"]) | |
a3acf0b0 | 139 | if exitcode != 0: |
215d7280 | 140 | # Racoon is finicky about its configuration file and will |
3c52fa7b JP |
141 | # refuse to start if it sees something it doesn't like |
142 | # (e.g., a certificate file doesn't exist). Try restarting | |
143 | # the process before giving up. | |
a251af0a | 144 | vlog.warn("attempting to restart racoon") |
b54bdbe9 BP |
145 | exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon", |
146 | "restart"]) | |
3c52fa7b | 147 | if exitcode != 0: |
a251af0a | 148 | vlog.warn("couldn't reload racoon") |
3c52fa7b JP |
149 | |
150 | def commit(self): | |
151 | # Rewrite the Racoon configuration file | |
b54bdbe9 | 152 | conf_file = open(root_prefix + self.conf_file, 'w') |
3c52fa7b JP |
153 | conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir)) |
154 | ||
155 | for host, vals in self.cert_hosts.iteritems(): | |
156 | conf_file.write(Racoon.cert_entry % (host, vals["certificate"], | |
157 | vals["private_key"], vals["peer_cert_file"])) | |
158 | ||
159 | for host in self.psk_hosts: | |
160 | conf_file.write(Racoon.psk_entry % host) | |
161 | ||
162 | conf_file.write(Racoon.conf_footer) | |
163 | conf_file.close() | |
164 | ||
165 | # Rewrite the pre-shared keys file; it must only be readable by root. | |
166 | orig_umask = os.umask(0077) | |
b54bdbe9 | 167 | psk_file = open(root_prefix + Racoon.psk_file, 'w') |
3c52fa7b JP |
168 | os.umask(orig_umask) |
169 | ||
170 | psk_file.write("# Generated by Open vSwitch...do not modify by hand!") | |
171 | psk_file.write("\n\n") | |
172 | for host, vals in self.psk_hosts.iteritems(): | |
173 | psk_file.write("%s %s\n" % (host, vals["psk"])) | |
174 | psk_file.close() | |
a3acf0b0 | 175 | |
3c52fa7b | 176 | self.reload() |
a3acf0b0 | 177 | |
3c52fa7b JP |
178 | def _add_psk(self, host, psk): |
179 | if host in self.cert_hosts: | |
180 | raise error.Error("host %s already defined for cert" % host) | |
a3acf0b0 | 181 | |
a3acf0b0 | 182 | self.psk_hosts[host] = psk |
3c52fa7b JP |
183 | self.commit() |
184 | ||
185 | def _verify_certs(self, vals): | |
186 | # Racoon will refuse to start if the certificate files don't | |
187 | # exist, so verify that they're there. | |
b54bdbe9 | 188 | if not os.path.isfile(root_prefix + vals["certificate"]): |
3c52fa7b JP |
189 | raise error.Error("'certificate' file does not exist: %s" |
190 | % vals["certificate"]) | |
b54bdbe9 | 191 | elif not os.path.isfile(root_prefix + vals["private_key"]): |
3c52fa7b JP |
192 | raise error.Error("'private_key' file does not exist: %s" |
193 | % vals["private_key"]) | |
194 | ||
195 | # Racoon won't start if a given certificate or private key isn't | |
196 | # valid. This is a weak test, but will detect the most flagrant | |
197 | # errors. | |
198 | if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1: | |
199 | raise error.Error("'peer_cert' is not in valid PEM format") | |
200 | ||
b54bdbe9 | 201 | cert = open(root_prefix + vals["certificate"]).read() |
3c52fa7b JP |
202 | if cert.find("-----BEGIN CERTIFICATE-----") == -1: |
203 | raise error.Error("'certificate' is not in valid PEM format") | |
204 | ||
b54bdbe9 | 205 | cert = open(root_prefix + vals["private_key"]).read() |
3c52fa7b JP |
206 | if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1: |
207 | raise error.Error("'private_key' is not in valid PEM format") | |
3c52fa7b JP |
208 | |
209 | def _add_cert(self, host, vals): | |
a3acf0b0 | 210 | if host in self.psk_hosts: |
3c52fa7b JP |
211 | raise error.Error("host %s already defined for psk" % host) |
212 | ||
ef7ee76a | 213 | if vals["certificate"] == None: |
3c52fa7b | 214 | raise error.Error("'certificate' not defined for %s" % host) |
ef7ee76a | 215 | elif vals["private_key"] == None: |
0f4d9dce | 216 | # Assume the private key is stored in the same PEM file as |
3c52fa7b JP |
217 | # the certificate. We make a copy of "vals" so that we don't |
218 | # modify the original "vals", which would cause the script | |
219 | # to constantly think that the configuration has changed | |
220 | # in the database. | |
221 | vals = vals.copy() | |
222 | vals["private_key"] = vals["certificate"] | |
223 | ||
224 | self._verify_certs(vals) | |
225 | ||
226 | # The peer's certificate comes to us in PEM format as a string. | |
227 | # Write that string to a file for Racoon to use. | |
12bb621f | 228 | f = open(root_prefix + vals["peer_cert_file"], "w") |
3c52fa7b JP |
229 | f.write(vals["peer_cert"]) |
230 | f.close() | |
231 | ||
3c52fa7b JP |
232 | self.cert_hosts[host] = vals |
233 | self.commit() | |
234 | ||
235 | def _del_cert(self, host): | |
236 | peer_cert_file = self.cert_hosts[host]["peer_cert_file"] | |
237 | del self.cert_hosts[host] | |
238 | self.commit() | |
239 | try: | |
b54bdbe9 | 240 | os.remove(root_prefix + peer_cert_file) |
3c52fa7b JP |
241 | except OSError: |
242 | pass | |
243 | ||
244 | def add_entry(self, host, vals): | |
245 | if vals["peer_cert"]: | |
246 | self._add_cert(host, vals) | |
247 | elif vals["psk"]: | |
248 | self._add_psk(host, vals) | |
249 | ||
250 | def del_entry(self, host): | |
251 | if host in self.cert_hosts: | |
252 | self._del_cert(host) | |
253 | elif host in self.psk_hosts: | |
a3acf0b0 | 254 | del self.psk_hosts[host] |
3c52fa7b | 255 | self.commit() |
a3acf0b0 JP |
256 | |
257 | ||
258 | # Class to configure IPsec on a system using racoon for IKE and setkey | |
259 | # for maintaining the Security Association Database (SAD) and Security | |
260 | # Policy Database (SPD). Only policies for GRE are supported. | |
261 | class IPsec: | |
262 | def __init__(self): | |
263 | self.sad_flush() | |
264 | self.spd_flush() | |
265 | self.racoon = Racoon() | |
3c52fa7b | 266 | self.entries = [] |
a3acf0b0 JP |
267 | |
268 | def call_setkey(self, cmds): | |
269 | try: | |
38aad449 | 270 | p = subprocess.Popen([root_prefix + SETKEY, "-c"], |
b54bdbe9 BP |
271 | stdin=subprocess.PIPE, |
272 | stdout=subprocess.PIPE) | |
a3acf0b0 | 273 | except: |
38aad449 | 274 | vlog.err("could not call %s%s" % (root_prefix, SETKEY)) |
a3acf0b0 JP |
275 | sys.exit(1) |
276 | ||
277 | # xxx It is safer to pass the string into the communicate() | |
278 | # xxx method, but it didn't work for slightly longer commands. | |
279 | # xxx An alternative may need to be found. | |
280 | p.stdin.write(cmds) | |
281 | return p.communicate()[0] | |
282 | ||
283 | def get_spi(self, local_ip, remote_ip, proto="esp"): | |
284 | # Run the setkey dump command to retrieve the SAD. Then, parse | |
285 | # the output looking for SPI buried in the output. Note that | |
286 | # multiple SAD entries can exist for the same "flow", since an | |
287 | # older entry could be in a "dying" state. | |
288 | spi_list = [] | |
289 | host_line = "%s %s" % (local_ip, remote_ip) | |
b54bdbe9 | 290 | results = self.call_setkey("dump ;\n").split("\n") |
a3acf0b0 JP |
291 | for i in range(len(results)): |
292 | if results[i].strip() == host_line: | |
293 | # The SPI is in the line following the host pair | |
0f4d9dce | 294 | spi_line = results[i + 1] |
a3acf0b0 JP |
295 | if (spi_line[1:4] == proto): |
296 | spi = spi_line.split()[2] | |
297 | spi_list.append(spi.split('(')[1].rstrip(')')) | |
298 | return spi_list | |
299 | ||
300 | def sad_flush(self): | |
b54bdbe9 | 301 | self.call_setkey("flush;\n") |
a3acf0b0 JP |
302 | |
303 | def sad_del(self, local_ip, remote_ip): | |
304 | # To delete all SAD entries, we should be able to use setkey's | |
305 | # "deleteall" command. Unfortunately, it's fundamentally broken | |
306 | # on Linux and not documented as such. | |
307 | cmds = "" | |
308 | ||
309 | # Delete local_ip->remote_ip SAD entries | |
310 | spi_list = self.get_spi(local_ip, remote_ip) | |
311 | for spi in spi_list: | |
312 | cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi) | |
313 | ||
314 | # Delete remote_ip->local_ip SAD entries | |
315 | spi_list = self.get_spi(remote_ip, local_ip) | |
316 | for spi in spi_list: | |
317 | cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi) | |
318 | ||
319 | if cmds: | |
320 | self.call_setkey(cmds) | |
321 | ||
322 | def spd_flush(self): | |
b54bdbe9 | 323 | self.call_setkey("spdflush;\n") |
a3acf0b0 JP |
324 | |
325 | def spd_add(self, local_ip, remote_ip): | |
f916d1cc | 326 | cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" % |
a3acf0b0 | 327 | (local_ip, remote_ip)) |
b54bdbe9 | 328 | cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" % |
a3acf0b0 JP |
329 | (remote_ip, local_ip)) |
330 | self.call_setkey(cmds) | |
331 | ||
332 | def spd_del(self, local_ip, remote_ip): | |
3c52fa7b | 333 | cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip) |
b54bdbe9 | 334 | cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip) |
a3acf0b0 JP |
335 | self.call_setkey(cmds) |
336 | ||
3c52fa7b JP |
337 | def add_entry(self, local_ip, remote_ip, vals): |
338 | if remote_ip in self.entries: | |
339 | raise error.Error("host %s already configured for ipsec" | |
340 | % remote_ip) | |
a3acf0b0 | 341 | |
3c52fa7b | 342 | self.racoon.add_entry(remote_ip, vals) |
a3acf0b0 JP |
343 | self.spd_add(local_ip, remote_ip) |
344 | ||
3c52fa7b | 345 | self.entries.append(remote_ip) |
a3acf0b0 | 346 | |
3c52fa7b JP |
347 | def del_entry(self, local_ip, remote_ip): |
348 | if remote_ip in self.entries: | |
349 | self.racoon.del_entry(remote_ip) | |
350 | self.spd_del(local_ip, remote_ip) | |
351 | self.sad_del(local_ip, remote_ip) | |
352 | ||
353 | self.entries.remove(remote_ip) | |
a3acf0b0 JP |
354 | |
355 | ||
3c52fa7b JP |
356 | def update_ipsec(ipsec, interfaces, new_interfaces): |
357 | for name, vals in interfaces.iteritems(): | |
358 | if name not in new_interfaces: | |
359 | ipsec.del_entry(vals["local_ip"], vals["remote_ip"]) | |
360 | ||
361 | for name, vals in new_interfaces.iteritems(): | |
362 | orig_vals = interfaces.get(name) | |
363 | if orig_vals: | |
364 | # Configuration for this host already exists. Check if it's | |
3831d6f4 JP |
365 | # changed. We use set difference, since we want to ignore |
366 | # any local additions to "orig_vals" that we've made | |
367 | # (e.g. the "peer_cert_file" key). | |
368 | if set(vals.items()) - set(orig_vals.items()): | |
3c52fa7b | 369 | ipsec.del_entry(vals["local_ip"], vals["remote_ip"]) |
3831d6f4 JP |
370 | else: |
371 | continue | |
3c52fa7b JP |
372 | |
373 | try: | |
374 | ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals) | |
375 | except error.Error, msg: | |
a251af0a | 376 | vlog.warn("skipping ipsec config for %s: %s" % (name, msg)) |
3c52fa7b | 377 | |
0f4d9dce | 378 | |
ef7ee76a | 379 | def get_ssl_cert(data): |
8cdf0349 | 380 | for ovs_rec in data["Open_vSwitch"].rows.itervalues(): |
ad6247f5 BP |
381 | if ovs_rec.ssl: |
382 | ssl = ovs_rec.ssl[0] | |
383 | if ssl.certificate and ssl.private_key: | |
384 | return (ssl.certificate, ssl.private_key) | |
ef7ee76a JP |
385 | |
386 | return None | |
387 | ||
0f4d9dce | 388 | |
b153e667 EJ |
389 | def main(): |
390 | ||
391 | parser = argparse.ArgumentParser() | |
392 | parser.add_argument("database", metavar="DATABASE", | |
393 | help="A socket on which ovsdb-server is listening.") | |
394 | parser.add_argument("--root-prefix", metavar="DIR", | |
395 | help="Use DIR as alternate root directory" | |
396 | " (for testing).") | |
0f4d9dce | 397 | |
27ae98ba | 398 | ovs.vlog.add_args(parser) |
b153e667 EJ |
399 | ovs.daemon.add_args(parser) |
400 | args = parser.parse_args() | |
27ae98ba | 401 | ovs.vlog.handle_args(args) |
b153e667 | 402 | ovs.daemon.handle_args(args) |
a3acf0b0 | 403 | |
b153e667 | 404 | global root_prefix |
c4f8424e EJ |
405 | if args.root_prefix: |
406 | root_prefix = args.root_prefix | |
8cdf0349 | 407 | |
b153e667 | 408 | remote = args.database |
bf42f674 EJ |
409 | schema_helper = ovs.db.idl.SchemaHelper() |
410 | schema_helper.register_columns("Interface", ["name", "type", "options"]) | |
411 | schema_helper.register_columns("Open_vSwitch", ["ssl"]) | |
412 | schema_helper.register_columns("SSL", ["certificate", "private_key"]) | |
413 | idl = ovs.db.idl.Idl(remote, schema_helper) | |
a3acf0b0 JP |
414 | |
415 | ovs.daemon.daemonize() | |
416 | ||
7b2d10c5 | 417 | ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None) |
53cf9963 | 418 | error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None) |
7b2d10c5 EJ |
419 | if error: |
420 | ovs.util.ovs_fatal(error, "could not create unixctl server", vlog) | |
421 | ||
a3acf0b0 JP |
422 | ipsec = IPsec() |
423 | ||
424 | interfaces = {} | |
6da258aa | 425 | seqno = idl.change_seqno # Sequence number when we last processed the db |
a3acf0b0 | 426 | while True: |
7b2d10c5 EJ |
427 | unixctl_server.run() |
428 | if exiting: | |
429 | break | |
430 | ||
6da258aa BP |
431 | idl.run() |
432 | if seqno == idl.change_seqno: | |
a3acf0b0 | 433 | poller = ovs.poller.Poller() |
7b2d10c5 | 434 | unixctl_server.wait(poller) |
a3acf0b0 JP |
435 | idl.wait(poller) |
436 | poller.block() | |
437 | continue | |
6da258aa | 438 | seqno = idl.change_seqno |
ef7ee76a | 439 | |
8cdf0349 | 440 | ssl_cert = get_ssl_cert(idl.tables) |
0f4d9dce | 441 | |
a3acf0b0 | 442 | new_interfaces = {} |
8cdf0349 | 443 | for rec in idl.tables["Interface"].rows.itervalues(): |
99e7b077 | 444 | if rec.type == "ipsec_gre": |
8cdf0349 BP |
445 | name = rec.name |
446 | options = rec.options | |
12bb621f | 447 | peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip")) |
ef7ee76a | 448 | entry = { |
8cdf0349 BP |
449 | "remote_ip": options.get("remote_ip"), |
450 | "local_ip": options.get("local_ip", "0.0.0.0/0"), | |
451 | "certificate": options.get("certificate"), | |
452 | "private_key": options.get("private_key"), | |
453 | "use_ssl_cert": options.get("use_ssl_cert"), | |
454 | "peer_cert": options.get("peer_cert"), | |
12bb621f | 455 | "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name, |
0f4d9dce | 456 | "psk": options.get("psk")} |
a3acf0b0 | 457 | |
ef7ee76a | 458 | if entry["peer_cert"] and entry["psk"]: |
a251af0a EJ |
459 | vlog.warn("both 'peer_cert' and 'psk' defined for %s" |
460 | % name) | |
3c52fa7b | 461 | continue |
ef7ee76a | 462 | elif not entry["peer_cert"] and not entry["psk"]: |
a251af0a | 463 | vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name) |
3c52fa7b | 464 | continue |
a3acf0b0 | 465 | |
ef7ee76a JP |
466 | # The "use_ssl_cert" option is deprecated and will |
467 | # likely go away in the near future. | |
468 | if entry["use_ssl_cert"] == "true": | |
469 | if not ssl_cert: | |
a251af0a | 470 | vlog.warn("no valid SSL entry for %s" % name) |
ef7ee76a JP |
471 | continue |
472 | ||
473 | entry["certificate"] = ssl_cert[0] | |
474 | entry["private_key"] = ssl_cert[1] | |
475 | ||
476 | new_interfaces[name] = entry | |
0f4d9dce | 477 | |
3c52fa7b JP |
478 | if interfaces != new_interfaces: |
479 | update_ipsec(ipsec, interfaces, new_interfaces) | |
a3acf0b0 | 480 | interfaces = new_interfaces |
0f4d9dce | 481 | |
7b2d10c5 EJ |
482 | unixctl_server.close() |
483 | idl.close() | |
484 | ||
0f4d9dce | 485 | |
a3acf0b0 JP |
486 | if __name__ == '__main__': |
487 | try: | |
b153e667 | 488 | main() |
a3acf0b0 JP |
489 | except SystemExit: |
490 | # Let system.exit() calls complete normally | |
491 | raise | |
492 | except: | |
27ae98ba | 493 | vlog.exception("traceback") |
55f8a832 | 494 | sys.exit(ovs.daemon.RESTART_EXIT_CODE) |