xenserver: Add vm-id to the external_ids.
[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     while True:
276         if not force_run and not idl.run():
277             poller = ovs.poller.Poller()
278             idl.wait(poller)
279             poller.block()
280             continue
281
282         if force_run:
283             vlog.info("Forced to re-run as the result of a SIGHUP")
284             bridges = {}
285             iface_ids = {}
286             vm_ids = {}
287             force_run = False
288
289         txn = ovs.db.idl.Transaction(idl)
290
291         new_bridges = {}
292         for row in idl.tables["Bridge"].rows.itervalues():
293             old_xnu = bridges.get(row.name)
294             new_xnu = row.external_ids.get("xs-network-uuids", "")
295             if old_xnu is None:
296                 # New bridge.
297                 update_fail_mode(row)
298                 update_in_band_mgmt(row)
299
300             update_bridge_id(row)
301             new_bridges[row.name] = new_xnu
302         bridges = new_bridges
303
304         iface_by_name = {}
305         for row in idl.tables["Interface"].rows.itervalues():
306             iface_by_name[row.name] = row
307
308         new_iface_ids = {}
309         new_vm_ids = {}
310         for row in idl.tables["Interface"].rows.itervalues():
311             # Match up paired vif and tap devices.
312             if row.name.startswith("vif"):
313                 vif = row
314                 tap = iface_by_name.get("tap%s" % row.name[3:])
315             elif row.name.startswith("tap"):
316                 tap = row
317                 vif = iface_by_name.get("vif%s" % row.name[3:])
318             else:
319                 tap = vif = None
320
321             # Several tap external-ids need to be copied from the vif.
322             if row == tap and vif:
323                 keys = ["attached-mac",
324                         "xs-network-uuid",
325                         "xs-vif-uuid",
326                         "xs-vm-uuid"]
327                 for k in keys:
328                     set_external_id(row, k, vif.external_ids.get(k))
329
330             # Map from xs-vif-uuid to iface-id.
331             #
332             # (A tap's xs-vif-uuid comes from its vif.  That falls out
333             # naturally from the copy loop above.)
334             xvu = row.external_ids.get("xs-vif-uuid")
335             if xvu:
336                 iface_id = (new_iface_ids.get(xvu)
337                             or iface_ids.get(xvu)
338                             or get_iface_id(row.name, xvu))
339                 new_iface_ids[xvu] = iface_id
340             else:
341                 # No xs-vif-uuid therefore no iface-id.
342                 iface_id = None
343             set_external_id(row, "iface-id", iface_id)
344
345             # Map from xs-vm-uuid to vm-id.
346             xvmu = row.external_ids.get("xs-vm-uuid")
347             if xvmu:
348                 vm_id = (new_vm_ids.get(xvmu)
349                          or vm_ids.get(xvmu)
350                          or get_vm_id(row.name, xvmu))
351                 new_vm_ids[xvmu] = vm_id
352             else:
353                 vm_id = None
354             set_external_id(row, "vm-id", vm_id)
355
356             # When there's a vif and a tap, the tap is active (used for
357             # traffic).  When there's just a vif, the vif is active.
358             #
359             # A tap on its own shouldn't happen, and we don't know
360             # anything about other kinds of devices, so we don't use
361             # an iface-status for those devices at all.
362             if vif and tap:
363                 set_external_id(tap, "iface-status", "active")
364                 set_external_id(vif, "iface-status", "inactive")
365             elif vif:
366                 set_external_id(vif, "iface-status", "active")
367             else:
368                 set_external_id(row, "iface-status", None)
369         iface_ids = new_iface_ids
370         vm_ids = new_vm_ids
371
372         txn.commit_block()
373
374
375 if __name__ == '__main__':
376     try:
377         main()
378     except SystemExit:
379         # Let system.exit() calls complete normally
380         raise
381     except:
382         vlog.exception("traceback")
383         sys.exit(ovs.daemon.RESTART_EXIT_CODE)