ovs-xapi-sync: Add unixctl support.
[sliver-openvswitch.git] / xenserver / usr_share_openvswitch_scripts_ovs-xapi-sync
1 #!/usr/bin/python
2 # Copyright (c) 2009, 2010, 2011, 2012 Nicira Networks
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 the external_ids columns of the Bridge and
18 # Interface OVSDB tables for changes that require interrogating XAPI.
19 # Its responsibilities include:
20 #
21 #   - Set the "bridge-id" key in the Bridge table.
22 #   - Set the "iface-id" key in the Interface table.
23 #   - Set the fail-mode on internal bridges.
24
25 import argparse
26 import os
27 import signal
28 import sys
29 import time
30
31 import XenAPI
32
33 import ovs.dirs
34 from ovs.db import error
35 from ovs.db import types
36 import ovs.daemon
37 import ovs.db.idl
38 import ovs.unixctl
39
40 vlog = ovs.vlog.Vlog("ovs-xapi-sync")
41 session = None
42 force_run = False
43 exiting = False
44
45
46 def unixctl_exit(conn, unused_argv, unused_aux):
47     global exiting
48     exiting = True
49     conn.reply(None)
50
51
52 # Set up a session to interact with XAPI.
53 #
54 # On system start-up, OVS comes up before XAPI, so we can't log into the
55 # session until later.  Try to do this on-demand, since we won't
56 # actually do anything interesting until XAPI is up.
57 def init_session():
58     global session
59     if session is not None:
60         return True
61
62     try:
63         session = XenAPI.xapi_local()
64         session.xenapi.login_with_password("", "")
65     except XenAPI.Failure, e:
66         session = None
67         vlog.warn("Couldn't login to XAPI (%s)" % e)
68         return False
69
70     return True
71
72
73 def get_network_by_bridge(br_name):
74     if not init_session():
75         vlog.warn("Failed to get bridge id %s because"
76                 " XAPI session could not be initialized" % br_name)
77         return None
78
79     for n in session.xenapi.network.get_all():
80         rec = session.xenapi.network.get_record(n)
81         if rec['bridge'] == br_name:
82             return rec
83
84     return None
85
86
87 # By default, the "bridge-id" external id in the Bridge table is the
88 # same as "xs-network-uuids".  This may be overridden by defining a
89 # "nicira-bridge-id" key in the "other_config" field of the network
90 # record of XAPI.  If nicira-bridge-id is undefined returns default.
91 # On error returns None.
92 def get_bridge_id(br_name, default=None):
93     rec = get_network_by_bridge(br_name)
94     if rec:
95         return rec['other_config'].get('nicira-bridge-id', default)
96     return None
97
98
99 # By default, the "iface-id" external id in the Interface table is the
100 # same as "xs-vif-uuid".  This may be overridden by defining a
101 # "nicira-iface-id" key in the "other_config" field of the VIF
102 # record of XAPI.
103 def get_iface_id(if_name, xs_vif_uuid):
104     if not if_name.startswith("vif") and not if_name.startswith("tap"):
105         # Treat whatever was passed into 'xs_vif_uuid' as a default
106         # value for non-VIFs.
107         return xs_vif_uuid
108
109     if not init_session():
110         vlog.warn("Failed to get interface id %s because"
111                 " XAPI session could not be initialized" % if_name)
112         return xs_vif_uuid
113
114     try:
115         vif = session.xenapi.VIF.get_by_uuid(xs_vif_uuid)
116         rec = session.xenapi.VIF.get_record(vif)
117         return rec['other_config'].get('nicira-iface-id', xs_vif_uuid)
118     except XenAPI.Failure:
119         vlog.warn("Could not find XAPI entry for VIF %s" % if_name)
120         return xs_vif_uuid
121
122
123 # By default, the "vm-id" external id in the Interface table is the
124 # same as "xs-vm-uuid".  This may be overridden by defining a
125 # "nicira-vm-id" key in the "other_config" field of the VM
126 # record of XAPI.
127 def get_vm_id(if_name, xs_vm_uuid):
128     if not if_name.startswith("vif") and not if_name.startswith("tap"):
129         # Treat whatever was passed into 'xs_vm_uuid' as a default
130         # value for non-VIFs.
131         return xs_vm_uuid
132
133     if not init_session():
134         vlog.warn("Failed to get vm id for interface id %s because"
135                 " XAPI session could not be initialized" % if_name)
136         return xs_vm_uuid
137
138     try:
139         vm = session.xenapi.VM.get_by_uuid(xs_vm_uuid)
140         rec = session.xenapi.VM.get_record(vm)
141         return rec['other_config'].get('nicira-vm-id', xs_vm_uuid)
142     except XenAPI.Failure:
143         vlog.warn("Could not find XAPI entry for VIF %s" % if_name)
144         return xs_vm_uuid
145
146
147 def set_or_delete(d, key, value):
148     if value is None:
149         if key in d:
150             del d[key]
151             return True
152     else:
153         if d.get(key) != value:
154             d[key] = value
155             return True
156     return False
157
158
159 def set_external_id(row, key, value):
160     external_ids = row.external_ids
161     if set_or_delete(external_ids, key, value):
162         row.external_ids = external_ids
163
164
165 # XenServer does not call interface-reconfigure on internal networks,
166 # which is where the fail-mode would normally be set.
167 def update_fail_mode(row):
168     rec = get_network_by_bridge(row.name)
169     if not rec:
170         return
171
172     fail_mode = rec['other_config'].get('vswitch-controller-fail-mode')
173
174     if not fail_mode:
175         pools = session.xenapi.pool.get_all()
176         if len(pools) == 1:
177             prec = session.xenapi.pool.get_record(pools[0])
178             fail_mode = prec['other_config'].get(
179                     'vswitch-controller-fail-mode')
180
181     if fail_mode not in ['standalone', 'secure']:
182         fail_mode = 'standalone'
183
184     if row.fail_mode != fail_mode:
185         row.fail_mode = fail_mode
186
187
188 def update_in_band_mgmt(row):
189     rec = get_network_by_bridge(row.name)
190     if not rec:
191         return
192
193     dib = rec['other_config'].get('vswitch-disable-in-band')
194
195     other_config = row.other_config
196     if dib and dib not in ['true', 'false']:
197         vlog.warn('"%s" isn\'t a valid setting for '
198                 "other_config:disable-in-band on %s" % (dib, row.name))
199     elif set_or_delete(other_config, 'disable-in-band', dib):
200         row.other_config = other_config
201
202
203 def keep_table_columns(schema, table_name, columns):
204     table = schema.tables.get(table_name)
205     if not table:
206         raise error.Error("schema has no %s table" % table_name)
207
208     new_columns = {}
209     for column_name in columns:
210         column = table.columns.get(column_name)
211         if not column:
212             raise error.Error("%s table schema lacks %s column"
213                               % (table_name, column_name))
214         new_columns[column_name] = column
215     table.columns = new_columns
216     return table
217
218
219 def prune_schema(schema):
220     new_tables = {}
221     new_tables["Bridge"] = keep_table_columns(
222         schema, "Bridge", ("name", "external_ids", "other_config",
223                            "fail_mode"))
224     new_tables["Interface"] = keep_table_columns(
225         schema, "Interface", ("name", "external_ids"))
226     schema.tables = new_tables
227
228
229 def handler(signum, _):
230     global force_run
231     if (signum == signal.SIGHUP):
232         force_run = True
233
234
235 def main():
236     global force_run
237
238     parser = argparse.ArgumentParser()
239     parser.add_argument("database", metavar="DATABASE",
240             help="A socket on which ovsdb-server is listening.")
241     parser.add_argument("--root-prefix", metavar="DIR", default='',
242                         help="Use DIR as alternate root directory"
243                         " (for testing).")
244
245     ovs.vlog.add_args(parser)
246     ovs.daemon.add_args(parser)
247     args = parser.parse_args()
248     ovs.vlog.handle_args(args)
249     ovs.daemon.handle_args(args)
250
251     remote = args.database
252     schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
253     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
254     prune_schema(schema)
255     idl = ovs.db.idl.Idl(remote, schema)
256
257     ovs.daemon.daemonize()
258
259     ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
260     error, unixctl_server = ovs.unixctl.UnixctlServer.create(None)
261     if error:
262         ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
263
264     # This daemon is usually started before XAPI, but to complete our
265     # tasks, we need it.  Wait here until it's up.
266     cookie_file = args.root_prefix + "/var/run/xapi_init_complete.cookie"
267     while not os.path.exists(cookie_file):
268         time.sleep(1)
269
270     signal.signal(signal.SIGHUP, handler)
271
272     bridges = {}                # Map from bridge name to nicira-bridge-id
273     iface_ids = {}              # Map from xs-vif-uuid to iface-id
274     vm_ids = {}                 # Map from xs-vm-uuid to vm-id
275     seqno = idl.change_seqno    # Sequence number when we last processed the db
276     while True:
277         unixctl_server.run()
278         if exiting:
279             break;
280
281         idl.run()
282         if not force_run and seqno == idl.change_seqno:
283             poller = ovs.poller.Poller()
284             unixctl_server.wait(poller)
285             idl.wait(poller)
286             poller.block()
287             continue
288
289         if force_run:
290             vlog.info("Forced to re-run as the result of a SIGHUP")
291             bridges = {}
292             iface_ids = {}
293             vm_ids = {}
294             force_run = False
295         seqno = idl.change_seqno
296
297         txn = ovs.db.idl.Transaction(idl)
298
299         new_bridges = {}
300         for row in idl.tables["Bridge"].rows.itervalues():
301             if row.name in bridges:
302                 nbd = bridges[row.name]
303             else:
304                 # New bridge.
305                 update_fail_mode(row)
306                 update_in_band_mgmt(row)
307                 nbd = get_bridge_id(row.name)
308
309             bridge_id = nbd
310             if bridge_id is None:
311                 bridge_id = row.external_ids.get("xs-network-uuids")
312
313             if bridge_id is not None:
314                 set_external_id(row, "bridge-id", bridge_id.split(";")[0])
315
316             new_bridges[row.name] = nbd
317         bridges = new_bridges
318
319         iface_by_name = {}
320         for row in idl.tables["Interface"].rows.itervalues():
321             iface_by_name[row.name] = row
322
323         new_iface_ids = {}
324         new_vm_ids = {}
325         for row in idl.tables["Interface"].rows.itervalues():
326             # Match up paired vif and tap devices.
327             if row.name.startswith("vif"):
328                 vif = row
329                 tap = iface_by_name.get("tap%s" % row.name[3:])
330             elif row.name.startswith("tap"):
331                 tap = row
332                 vif = iface_by_name.get("vif%s" % row.name[3:])
333             else:
334                 tap = vif = None
335
336             # Several tap external-ids need to be copied from the vif.
337             if row == tap and vif:
338                 keys = ["attached-mac",
339                         "xs-network-uuid",
340                         "xs-vif-uuid",
341                         "xs-vm-uuid"]
342                 for k in keys:
343                     set_external_id(row, k, vif.external_ids.get(k))
344
345             # Map from xs-vif-uuid to iface-id.
346             #
347             # (A tap's xs-vif-uuid comes from its vif.  That falls out
348             # naturally from the copy loop above.)
349             xvu = row.external_ids.get("xs-vif-uuid")
350             if xvu:
351                 iface_id = (new_iface_ids.get(xvu)
352                             or iface_ids.get(xvu)
353                             or get_iface_id(row.name, xvu))
354                 new_iface_ids[xvu] = iface_id
355             else:
356                 # No xs-vif-uuid therefore no iface-id.
357                 iface_id = None
358             set_external_id(row, "iface-id", iface_id)
359
360             # Map from xs-vm-uuid to vm-id.
361             xvmu = row.external_ids.get("xs-vm-uuid")
362             if xvmu:
363                 vm_id = (new_vm_ids.get(xvmu)
364                          or vm_ids.get(xvmu)
365                          or get_vm_id(row.name, xvmu))
366                 new_vm_ids[xvmu] = vm_id
367             else:
368                 vm_id = None
369             set_external_id(row, "vm-id", vm_id)
370
371             # When there's a vif and a tap, the tap is active (used for
372             # traffic).  When there's just a vif, the vif is active.
373             #
374             # A tap on its own shouldn't happen, and we don't know
375             # anything about other kinds of devices, so we don't use
376             # an iface-status for those devices at all.
377             if vif and tap:
378                 set_external_id(tap, "iface-status", "active")
379                 set_external_id(vif, "iface-status", "inactive")
380             elif vif:
381                 set_external_id(vif, "iface-status", "active")
382             else:
383                 set_external_id(row, "iface-status", None)
384         iface_ids = new_iface_ids
385         vm_ids = new_vm_ids
386
387         txn.commit_block()
388
389     unixctl_server.close()
390     idl.close()
391
392
393 if __name__ == '__main__':
394     try:
395         main()
396     except SystemExit:
397         # Let system.exit() calls complete normally
398         raise
399     except:
400         vlog.exception("traceback")
401         sys.exit(ovs.daemon.RESTART_EXIT_CODE)