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