Files
NetworkManager/libnm/generate-setting-docs.py
Thomas Haller 13f6f3a410 libnm: rework team handling of JSON config
Completely refactor the team/JSON handling in libnm's NMSettingTeam and
NMSettingTeamPort.

- team handling was added as rh#1398925. The goal is to have a more
  convenient way to set properties than constructing JSON. This requires
  libnm to implement the hard task of parsing JSON (and exposing well-understood
  properties) and generating JSON (based on these "artificial" properties).
  But not only libnm. In particular nmcli and the D-Bus API must make this
  "simpler" API accessible.

- since NMSettingTeam and NMSettingTeamPort are conceptually the same,
  add "libnm-core/nm-team-utils.h" and NMTeamSetting that tries to
  handle the similar code side-by-sdie.
  The setting classes now just delegate for everything to NMTeamSetting.

- Previously, there was a very fuzzy understanding of the provided
  JSON config. Tighten that up, when setting a JSON config it
  regenerates/parses all other properties and tries to make the
  best of it. When modifying any abstraction property, the entire
  JSON config gets regenerated. In particular, don't try to merge
  existing JSON config with the new fields. If the user uses the
  abstraction API, then the entire JSON gets replaced.

  For example note that nm_setting_team_add_link_watcher() would not
  be reflected in the JSON config (a bug). That only accidentally worked
  because client would serializing the changed link watcher to
  GVariant/D-Bus, then NetworkManager would set it via g_object_set(),
  which would renerate the JSON, and finally persist it to disk. But
  as far as libnm is concerned, nm_setting_team_add_link_watcher() would
  bring the settings instance in an inconsistent state where JSON and
  the link watcher property disagree. Setting any property must
  immediately update both the JSON and the abstraction API.

- when constucting a team setting from D-Bus, we would previously parse
  both "config" and abstraction properties. That is wrong. Since our
  settings plugins only support JSON, all information must be present
  in the JSON config anyway. So, when "config" is present, only the JSON
  must be parsed. In the best case, the other information is redudant and
  contributes nothing. In the worse case, they information differs
  (which might happen if the client version differs from the server
  version). As the settings plugin only supports JSON, it's wrong to
  consider redundant, differing information from D-Bus.

- we now only convert string to JSON or back when needed. Previously,
  setting a property resulted in parsing several JSON multiple times
  (per property). All operations should now scale well and be reasonably
  efficient.

- also the property-changed signals are now handled correctly. Since
  NMTeamSetting knows the current state of all attributes, it can emit
  the exact property changed signals for what changed.

- we no longer use libjansson to generate the JSON. JSON is supposed
  to be a machine readable exchange format, hence a major goal is
  to be easily handled by applications. While parsing JSON is not so
  trivial, writing a well-known set of values to JSON is.
  The advantage is that when you build libnm without libjansson support,
  then we still can convert the artificial properties to JSON.

- Requiring libjansson in libnm is a burden, because most of the time
  it is not needed (as most users don't create team configurations). With
  this change we only require it to parse the team settings (no longer to
  write them). It should be reasonably simple to use a more minimalistic
  JSON parser that is sufficient for us, so that we can get rid of the
  libjansson dependency (for libnm). This also avoids the pain that we have
  due to the symbol collision of libjansson and libjson-glib.

https://bugzilla.redhat.com/show_bug.cgi?id=1691619
2019-05-23 18:09:49 +02:00

281 lines
10 KiB
Python
Executable File

#!/usr/bin/env python
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301 USA.
#
# Copyright 2009 - 2017 Red Hat, Inc.
from __future__ import print_function
import os
import gi
gi.require_version('GIRepository', '2.0')
from gi.repository import GIRepository
import argparse, datetime, re, sys
import xml.etree.ElementTree as ET
try:
libs = os.environ['LD_LIBRARY_PATH'].split(':')
libs.reverse()
for lib in libs:
GIRepository.Repository.prepend_library_path(lib)
except AttributeError:
# An old GI version, that has no prepend_library_path
# It's alright, it probably interprets LD_LIBRARY_PATH
# correctly.
pass
except KeyError:
pass
gi.require_version('NM', '1.0')
from gi.repository import NM, GObject
dbus_type_name_map = {
'b': 'boolean',
's': 'string',
'i': 'int32',
'u': 'uint32',
't': 'uint64',
'x': 'int64',
'y': 'byte',
'as': 'array of string',
'au': 'array of uint32',
'ay': 'byte array',
'a{ss}': 'dict of string to string',
'a{sv}': 'vardict',
'aa{sv}': 'array of vardict',
'aau': 'array of array of uint32',
'aay': 'array of byte array',
'a(ayuay)': 'array of legacy IPv6 address struct',
'a(ayuayu)': 'array of legacy IPv6 route struct',
'aa{sv}': 'array of vardict',
}
ns_map = {
'c': 'http://www.gtk.org/introspection/c/1.0',
'gi': 'http://www.gtk.org/introspection/core/1.0',
'glib': 'http://www.gtk.org/introspection/glib/1.0'
}
identifier_key = '{%s}identifier' % ns_map['c']
nick_key = '{%s}nick' % ns_map['glib']
symbol_prefix_key = '{%s}symbol-prefix' % ns_map['c']
constants = {
'TRUE': 'TRUE',
'FALSE': 'FALSE',
'G_MAXUINT32': 'G_MAXUINT32',
'NULL': 'NULL' }
setting_names = {}
def get_setting_name_define(setting):
n = setting.attrib[symbol_prefix_key]
if n and n.startswith("setting_"):
return n[8:].upper()
raise Exception("Unexpected symbol_prefix_key \"%s\"" % (n))
def init_constants(girxml, settings):
for const in girxml.findall('./gi:namespace/gi:constant', ns_map):
cname = const.attrib['{%s}type' % ns_map['c']]
cvalue = const.attrib['value']
if const.find('./gi:type[@name="utf8"]', ns_map) is not None:
cvalue = '"%s"' % cvalue
constants[cname] = cvalue
for enum in girxml.findall('./gi:namespace/gi:enumeration', ns_map):
for enumval in enum.findall('./gi:member', ns_map):
cname = enumval.attrib[identifier_key]
cvalue = '%s (%s)' % (cname, enumval.attrib['value'])
constants[cname] = cvalue
for enum in girxml.findall('./gi:namespace/gi:bitfield', ns_map):
for enumval in enum.findall('./gi:member', ns_map):
cname = enumval.attrib[identifier_key]
cvalue = '%s (0x%x)' % (cname, int(enumval.attrib['value']))
constants[cname] = cvalue
for setting in settings:
setting_type_name = 'NM' + setting.attrib['name'];
setting_name_symbol = 'NM_SETTING_' + get_setting_name_define(setting) + '_SETTING_NAME'
if setting_name_symbol in constants:
setting_name = constants[setting_name_symbol]
setting_names[setting_type_name] = setting_name
def get_prop_type(setting, pspec):
dbus_type = setting.get_dbus_property_type(pspec.name).dup_string()
prop_type = dbus_type_name_map[dbus_type]
if GObject.type_is_a(pspec.value_type, GObject.TYPE_ENUM) or GObject.type_is_a(pspec.value_type, GObject.TYPE_FLAGS):
prop_type = "%s (%s)" % (pspec.value_type.name, prop_type)
return prop_type
def get_docs(propxml):
doc_xml = propxml.find('gi:doc', ns_map)
if doc_xml is None:
return None
doc = doc_xml.text
if 'deprecated' in propxml.attrib:
doc = doc + ' Deprecated: ' + propxml.attrib['deprecated']
doc = re.sub(r'\n\s*', r' ', doc)
# Expand constants
doc = re.sub(r'%([^%]\w*)', lambda match: constants[match.group(1)], doc)
# #NMSettingWired:mac-address -> "mac-address"
doc = re.sub(r'#[A-Za-z0-9_]*:([A-Za-z0-9_-]*)', r'"\1"', doc)
# #NMSettingWired setting -> "802-3-ethernet" setting
doc = re.sub(r'#([A-Z]\w*) setting', lambda match: setting_names[match.group(1)] + ' setting', doc)
# remaining gtk-doc cleanup
doc = doc.replace('%%', '%')
doc = doc.replace('<!-- -->', '')
doc = re.sub(r' Element-.ype:.*', '', doc)
doc = re.sub(r'#([A-Z]\w*)', r'\1', doc)
# Remove sentences that refer to functions
doc = re.sub(r'\.\s+[^.]*\w\(\)[^.]*\.', r'.', doc)
return doc
def get_default_value(setting, pspec, propxml):
default_value = setting.get_property(pspec.name.replace('-', '_'))
if default_value is None:
return default_value
value_type = get_prop_type(setting, pspec)
if value_type == 'string' and default_value != '' and pspec.name != 'name':
default_value = '"%s"' % default_value
elif value_type == 'boolean':
default_value = str(default_value).upper()
elif value_type == 'byte array':
default_value = '[]'
elif str(default_value).startswith('<'):
default_value = None
elif str(default_value).startswith('['):
default_value = None
return default_value
def settings_sort_key(x):
x_prefix = x.attrib['{%s}symbol-prefix' % ns_map['c']]
# always sort NMSettingConnection first
return (x_prefix != "setting_connection", x_prefix);
def escape(val):
return str(val).replace('"', '&quot;')
def usage():
print("Usage: %s --gir FILE --output FILE" % sys.argv[0])
exit()
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--lib-path', metavar='PATH', action='append', help='path to scan for shared libraries')
parser.add_argument('-g', '--gir', metavar='FILE', help='NM-1.0.gir file')
parser.add_argument('-x', '--overrides', metavar='FILE', help='documentation overrides file')
parser.add_argument('-o', '--output', metavar='FILE', help='output file')
args = parser.parse_args()
if args.gir is None or args.output is None:
usage()
if args.lib_path:
for lib in args.lib_path:
GIRepository.Repository.prepend_library_path(lib)
girxml = ET.parse(args.gir).getroot()
outfile = open(args.output, mode='w')
basexml = girxml.find('./gi:namespace/gi:class[@name="Setting"]', ns_map)
settings = girxml.findall('./gi:namespace/gi:class[@parent="Setting"]', ns_map)
# Hack. Need a better way to do this
ipxml = girxml.find('./gi:namespace/gi:class[@name="SettingIPConfig"]', ns_map)
settings.extend(girxml.findall('./gi:namespace/gi:class[@parent="SettingIPConfig"]', ns_map))
settings = sorted(settings, key=settings_sort_key)
init_constants(girxml, settings)
if args.overrides is not None:
overrides = ET.parse(args.overrides).getroot()
outfile.write("""<?xml version=\"1.0\"?>
<!DOCTYPE nm-setting-docs [
<!ENTITY quot "&#34;">
]>
<nm-setting-docs>
""")
for settingxml in settings:
if 'abstract' in settingxml.attrib:
continue
new_func = NM.__getattr__(settingxml.attrib['name'])
setting = new_func()
class_desc = get_docs(settingxml)
if class_desc is None:
raise Exception("%s needs a gtk-doc block with one-line description" % setting.props.name)
outfile.write(" <setting name=\"%s\" description=\"%s\" name_upper=\"%s\" >\n" % (setting.props.name, class_desc, get_setting_name_define (settingxml)))
setting_properties = { prop.name: prop for prop in GObject.list_properties(setting) if prop.name != 'name' }
if args.overrides is None:
setting_overrides = {}
else:
setting_overrides = { override.attrib['name']: override for override in overrides.findall('./setting[@name="%s"]/property' % setting.props.name) }
properties = sorted(set.union(set(setting_properties.keys()), set(setting_overrides.keys())))
for prop in properties:
value_type = None
value_desc = None
default_value = None
if prop in setting_properties:
pspec = setting_properties[prop]
propxml = settingxml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
if propxml is None:
propxml = basexml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
if propxml is None:
propxml = ipxml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
value_type = get_prop_type(setting, pspec)
value_desc = get_docs(propxml)
default_value = get_default_value(setting, pspec, propxml)
if prop in setting_overrides:
override = setting_overrides[prop]
if override.attrib['format'] != '':
value_type = override.attrib['format']
if override.attrib['description'] != '':
value_desc = override.attrib['description']
prop_upper = prop.upper().replace('-', '_')
if value_desc is None:
raise Exception("%s.%s needs a documentation description" % (setting.props.name, prop))
if default_value is not None:
outfile.write(" <property name=\"%s\" name_upper=\"%s\" type=\"%s\" default=\"%s\" description=\"%s\" />\n" %
(prop, prop_upper, value_type, escape(default_value), escape(value_desc)))
else:
outfile.write(" <property name=\"%s\" name_upper=\"%s\" type=\"%s\" description=\"%s\" />\n" %
(prop, prop_upper, value_type, escape(value_desc)))
outfile.write(" </setting>\n")
outfile.write("</nm-setting-docs>\n")
outfile.close()