From 604bb9f9fa963bd381f7ece4bdedbb13f57cf7c7 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Tue, 4 Oct 2022 11:20:48 +0200 Subject: [PATCH 1/6] libnm/docs: fix alignment in BOOTPROTO description The parser will become more strict about whitespace. Don't have these whitespaces, they will be kept. --- src/libnm-core-impl/nm-setting-ip4-config.c | 10 +++++----- src/libnm-core-impl/nm-setting-ip6-config.c | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libnm-core-impl/nm-setting-ip4-config.c b/src/libnm-core-impl/nm-setting-ip4-config.c index 46ad495092..d1aa72ae25 100644 --- a/src/libnm-core-impl/nm-setting-ip4-config.c +++ b/src/libnm-core-impl/nm-setting-ip4-config.c @@ -624,9 +624,9 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass) /* ---ifcfg-rh--- * property: method * variable: BOOTPROTO - * format: string - * values: none, dhcp (bootp), static, ibft, autoip, shared - * default: none + * format: string + * values: none, dhcp (bootp), static, ibft, autoip, shared + * default: none * description: Method used for IPv4 protocol configuration. * ---end--- */ @@ -641,7 +641,7 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass) /* ---ifcfg-rh--- * property: dns * variable: DNS1, DNS2, ... - * format: string + * format: string * description: List of DNS servers. Even if NetworkManager supports many DNS * servers, initscripts and resolver only care about the first three, usually. * example: DNS1=1.2.3.4 DNS2=10.0.0.254 DNS3=8.8.8.8 @@ -651,7 +651,7 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass) /* ---ifcfg-rh--- * property: dns-search * variable: DOMAIN - * format: string (space-separated domains) + * format: string (space-separated domains) * description: List of DNS search domains. * ---end--- */ diff --git a/src/libnm-core-impl/nm-setting-ip6-config.c b/src/libnm-core-impl/nm-setting-ip6-config.c index c6288b2694..01b6855ef2 100644 --- a/src/libnm-core-impl/nm-setting-ip6-config.c +++ b/src/libnm-core-impl/nm-setting-ip6-config.c @@ -563,7 +563,7 @@ nm_setting_ip6_config_class_init(NMSettingIP6ConfigClass *klass) /* ---ifcfg-rh--- * property: method * variable: IPV6INIT, IPV6FORWARDING, IPV6_AUTOCONF, DHCPV6C, IPV6_DISABLED - * default: IPV6INIT=yes; IPV6FORWARDING=no; IPV6_AUTOCONF=!IPV6FORWARDING, DHCPV6=no + * default: IPV6INIT=yes; IPV6FORWARDING=no; IPV6_AUTOCONF=!IPV6FORWARDING, DHCPV6=no * description: Method used for IPv6 protocol configuration. * ignore ~ IPV6INIT=no; auto ~ IPV6_AUTOCONF=yes; dhcp ~ IPV6_AUTOCONF=no and DHCPV6C=yes; * disabled ~ IPV6_DISABLED=yes @@ -580,7 +580,7 @@ nm_setting_ip6_config_class_init(NMSettingIP6ConfigClass *klass) /* ---ifcfg-rh--- * property: dns * variable: DNS1, DNS2, ... - * format: string + * format: string * description: List of DNS servers. NetworkManager uses the variables both * for IPv4 and IPv6. * ---end--- @@ -589,7 +589,7 @@ nm_setting_ip6_config_class_init(NMSettingIP6ConfigClass *klass) /* ---ifcfg-rh--- * property: dns-search * variable: IPV6_DOMAIN(+) - * format: string (space-separated domains) + * format: string (space-separated domains) * description: List of DNS search domains. * ---end--- */ From 8fc7b6df12edfdf9a4e74d2bc77853f2806618e8 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Mon, 29 Aug 2022 10:37:06 +0200 Subject: [PATCH 2/6] tools: rework generating documentation from libnm meta data With the given input, this produces *exactly* the same XML as before. - the parsing is now stricter (and thus the code more verbose). No funny stuff, get the annotations correct. - on parsing errors, we log now the affecting lines - "nm-setting-ip-config.c" is a base class. Previously it was ignored and for the moment we still do that. Next, we will allow to also describe properties there. - prepare the code to better preserve whitespace, indentation and line wrappings. In particular, to honor a blank line to indicate a line break and support paragraphs. This is not yet done to compare the output to before, but will be turned on with a small patch next. - the code will make it simple to promote the XML attributes to nodes. Attributes aren't great, let's write XML nodes later. We will only need to adjust the "keywords" dictionary for that, but this change will require changes to the entire chain of tools. --- tools/generate-docs-nm-property-infos.py | 499 +++++++++++++++++------ 1 file changed, 384 insertions(+), 115 deletions(-) diff --git a/tools/generate-docs-nm-property-infos.py b/tools/generate-docs-nm-property-infos.py index 25aa272a69..568bab17bc 100755 --- a/tools/generate-docs-nm-property-infos.py +++ b/tools/generate-docs-nm-property-infos.py @@ -1,143 +1,412 @@ #!/usr/bin/env python # SPDX-License-Identifier: LGPL-2.1-or-later +import os import re import sys +import collections import xml.etree.ElementTree as ET -def get_setting_name(one_file): - setting_name = "" - assert re.match(r".*/libnm-core-impl/nm-setting-.*\.c$", one_file) - header_path = one_file.replace("libnm-core-impl", "libnm-core-public") - header_path = header_path.replace(".c", ".h") +class LineError(Exception): + def __init__(self, line_no, msg): + Exception.__init__(self, msg) + self.line_no = line_no + + +_dbg_level = 0 +try: + _dbg_level = int(os.getenv("NM_DEBUG_GENERATE_DOCS", 0)) +except Exception: + pass + + +def dbg(msg, level=1): + if level <= _dbg_level: + print(msg) + + +def iter_unique(iterable, default=None): + found = False + for i in iterable: + assert not found + found = True + i0 = i + if found: + return i0 + return default + + +def xnode_get_or_create(root_node, node_name, name): + # From root_node, get the node "<{node_name} name={name} .../>" + # or create one, if it doesn't exist. + node = iter_unique( + (node for node in root_node.findall(node_name) if node.attrib["name"] == name) + ) + if node is None: + created = True + node = ET.SubElement(root_node, node_name, name=name) + else: + created = False + + return node, created + + +def get_setting_names(source_file): + m = re.match(r"^(.*)/libnm-core-impl/(nm-setting-[^/]*)\.c$", source_file) + assert m + + path_prefix, file_base = (m.group(1), m.group(2)) + + if file_base == "nm-setting-ip-config": + # Special case ip-config, which is a base class. + return None + + header_file = "%s/libnm-core-public/%s.h" % (path_prefix, file_base) + try: - header_reader = open(header_path, "r") + f = open(header_file, "r") except OSError: - print("Can not open header file: %s" % (header_path)) - exit(1) + raise Exception( + 'Can not open header file "%s" for "%s"' % (header_file, source_file) + ) - line = header_reader.readline() - while line != "": - setting_name_found = re.search(r"NM_SETTING_.+SETTING_NAME\s+\"(\S+)\"", line) - if setting_name_found: - setting_name = setting_name_found.group(1) - break - line = header_reader.readline() - header_reader.close() - return setting_name + with f: + for line in f: + m = re.search(r"^#define +NM_SETTING_.+SETTING_NAME\s+\"(\S+)\"$", line) + if m: + return m.group(1) + + raise Exception( + 'Can\'t find setting name in header file "%s" for "%s"' + % (header_file, source_file) + ) -def scan_doc_comments(plugin, setting_node, file, start_tag, end_tag): - data = [] - push_flag = 0 - try: - file_reader = open(file, "r") - except OSError: - print("Can not open file: %s" % (file)) - exit(1) - - line = file_reader.readline() - while line != "": - if start_tag in line: - push_flag = 1 - elif end_tag in line and push_flag == 1: - push_flag = 0 - parsed_data = process_data(data) - if parsed_data: - write_data(setting_node, parsed_data) - data = [] - elif push_flag == 1: - data.append(line) - line = file_reader.readline() - file_reader.close() - return +def get_file_infos(source_files): + for source_file in source_files: + setting_name = get_setting_names(source_file) + if setting_name: + yield setting_name, source_file -keywords = [ - "property", - "variable", - "format", - "values", - "default", - "example", - "description", - "description-docbook", -] -kwd_first_line_re = re.compile( - r"^\s*\**\s+({}):\s+(.*?)\s*$".format("|".join(keywords)) +KEYWORD_XML_TYPE_NESTED = "nested" +KEYWORD_XML_TYPE_NODE = "node" +KEYWORD_XML_TYPE_ATTR = "attr" + +keywords = collections.OrderedDict( + [ + ("property", KEYWORD_XML_TYPE_ATTR), + ("variable", KEYWORD_XML_TYPE_ATTR), + ("format", KEYWORD_XML_TYPE_ATTR), + ("values", KEYWORD_XML_TYPE_ATTR), + ("default", KEYWORD_XML_TYPE_ATTR), + ("example", KEYWORD_XML_TYPE_ATTR), + ("description", KEYWORD_XML_TYPE_ATTR), + ("description-docbook", KEYWORD_XML_TYPE_NESTED), + ] ) -kwd_more_line_re = re.compile(r"^\s*\**\s+(.*?)\s*$") -def process_data(data): - parsed_data = {} - if not data: - return parsed_data - keyword = "" - for line in data: - kwd_first_line_found = kwd_first_line_re.search(line) - if kwd_first_line_found: - keyword = kwd_first_line_found.group(1) - if keyword == "description-docbook": - value = kwd_first_line_found.group(2) + "\n" - else: - value = kwd_first_line_found.group(2) + " " - parsed_data[keyword] = value +def keywords_allowed(tag, keyword): + # certain keywords might not be valid for some tags. + # Currently, all of them are always valid. + assert keyword in keywords + return True + + +def write_data(tag, setting_node, line_no, parsed_data): + + for k in parsed_data.keys(): + assert keywords_allowed(tag, k) + assert k in keywords + + name = parsed_data["property"] + property_node, created = xnode_get_or_create(setting_node, "property", name) + if not created: + raise LineError(line_no, 'Duplicate property %s" % (k, v, k)) + property_node.append(des) + elif xmltype == KEYWORD_XML_TYPE_NODE: + node = ET.SubElement(property_node, k) + node.text = v + elif xmltype == KEYWORD_XML_TYPE_ATTR: + property_node.set(k, v) + else: + assert False + + +kwd_first_line_re = re.compile(r"^ *\* ([-a-z0-9]+): (.*)$") +kwd_more_line_re = re.compile(r"^ *\*( *)(.*?)\s*$") + + +def parse_data(tag, line_no, lines): + assert lines + parsed_data = {} + keyword = "" + first_line = True + indent = None + for line in lines: + assert "\n" not in line + line_no += 1 + m = re.search(r"^ \*(| .*)$", line) + if not m: + raise LineError(line_no, 'Invalid formatted line "%s"' % (line,)) + content = m.group(1) + + m = re.search("^ ([-a-z0-9]+):(.*)$", content) + text_keyword_started = None + if m: + keyword = m.group(1) + if keyword in parsed_data: + raise LineError(line_no, 'Duplicated keyword "%s"' % (keyword,)) + text = m.group(2) + text_keyword_started = text + if text: + if text[0] != " " or len(text) == 1: + raise LineError(line_no, 'Invalid formatted line "%s"' % (line,)) + text = text[1:] + if not keywords_allowed(tag, keyword): + raise LineError(line_no, 'Invalid key "%s" for %s' % (keyword, tag)) + if parsed_data and keyword == "property": + raise LineError(line_no, 'The "property:" keywork must be first') + parsed_data[keyword] = text + new_keyword_stated = True + indent = None + else: + if content == "": + text = "" + elif content[0] == " " and len(content) > 1: + text = content[1:] + assert text + if indent is None: + indent = re.search("^( *)", text).group(1) + if not text.startswith(indent): + raise LineError(line_no, 'Unexpected indention in "%s"' % (line,)) + text = text[len(indent) :] + else: + raise LineError(line_no, 'Unexpected line "%s"' % (line,)) + if not keyword: + raise LineError(line_no, "Expected data in comment: %s" % (line)) + if text and text[0] == "\\": + assert False + text = text[1:] + if separator == " " and text == "": + # No separator to add. This is a blank line + pass + else: + parsed_data[keyword] = parsed_data[keyword] + separator + text.strip() + + if keywords[keyword] == KEYWORD_XML_TYPE_NESTED: + # This is plain XML. They lines are joined by newlines. + separator = "\n" + elif text_keyword_started == "": + # If the previous line was just "tag:$", we don't need a separator + # the next time. + separator = "" + elif not text: + # A blank line is used to mark a line break, while otherwise + # lines are joined by space. + separator = " " + else: + separator = " " + if "property" not in parsed_data: + raise LineError(line_no, 'Missing "property:" tag') + for keyword in keywords.keys(): + if not keywords_allowed(tag, keyword): + continue + if keyword not in parsed_data: + parsed_data[keyword] = None return parsed_data -def write_data(setting_node, parsed_data): - property_node = ET.SubElement(setting_node, "property") - property_node.set("name", parsed_data["property"]) - property_node.set("variable", parsed_data["variable"]) - property_node.set("format", parsed_data["format"]) - property_node.set("values", parsed_data["values"]) - property_node.set("default", parsed_data["default"]) - property_node.set("example", parsed_data["example"]) - property_node.set("description", parsed_data["description"]) - if parsed_data["description-docbook"]: - des = ET.fromstring( - "" - + parsed_data["description-docbook"] - + "" - ) - property_node.append(des) +def process_setting(tag, root_node, source_file, setting_name): + dbg( + "> > tag:%s, source_file:%s, setting_name:%s" % (tag, source_file, setting_name) + ) -if len(sys.argv) < 4: - print("Usage: %s [plugin] [output-xml-file] [srcfiles]" % (sys.argv[0])) - exit(1) + start_tag = "---" + tag + "---" + end_tag = "---end---" -argv = list(sys.argv[1:]) -plugin, output, source_files = argv[0], argv[1], argv[2:] -start_tag = "---" + plugin + "---" -end_tag = "---end---" -root_node = ET.Element("nm-setting-docs") - -for one_file in source_files: - setting_name = get_setting_name(one_file) - if setting_name: - setting_node = ET.SubElement(root_node, "setting", name=setting_name) + setting_node, created = xnode_get_or_create(root_node, "setting", setting_name) + if created: setting_node.text = "\n" - scan_doc_comments(plugin, setting_node, one_file, start_tag, end_tag) -ET.ElementTree(root_node).write(output) + try: + f = open(source_file, "r") + except OSError: + raise Exception("Can not open file: %s" % (source_file)) + + lines = None + with f: + line_no = 0 + just_had_end_tag = False + line_no_start = None + for line in f: + line_no += 1 + if line and line[-1] == "\n": + line = line[:-1] + if just_had_end_tag: + # After the end-tag, we still expect one particular line. Be strict about + # this. + just_had_end_tag = False + if line != " */": + raise LineError( + line_no, + 'Invalid end tag "%s". Expects literally " */" after end-tag' + % (line,), + ) + elif start_tag in line: + if line != " /* " + start_tag: + raise LineError( + line_no, + 'Invalid start tag "%s". Expects literally " /* %s"' + % (line, start_tag), + ) + if lines is not None: + raise LineError( + line_no, 'Invalid start tag "%s", missing end-tag' % (line,) + ) + lines = [] + line_no_start = line_no + elif end_tag in line and lines is not None: + if line != " * " + end_tag: + raise LineError(line_no, 'Invalid end tag: "%s"' % (line,)) + parsed_data = parse_data(tag, line_no_start, lines) + if not parsed_data: + raise Exception('invalid data: line %s, "%s"' % (line_no, lines)) + dbg("> > > property: %s" % (parsed_data["property"],)) + if _dbg_level > 1: + for keyword in sorted(parsed_data.keys()): + v = parsed_data[keyword] + if v is not None: + v = '"%s"' % (v,) + dbg( + "> > > > [%s] (%s) = %s" % (keyword, keywords[keyword], v), + level=2, + ) + write_data(tag, setting_node, line_no_start, parsed_data) + lines = None + elif lines is not None: + lines.append(line) + if lines is not None or just_had_end_tag: + raise LineError(line_no_start, "Unterminated start tag") + + +def process_settings_docs(tag, output, source_files): + + dbg("> tag:%s, output:%s" % (tag, output)) + + root_node = ET.Element("nm-setting-docs") + + for setting_name, source_file in get_file_infos(source_files): + try: + process_setting(tag, root_node, source_file, setting_name) + except LineError as e: + raise Exception( + "Error parsing %s, line %s (tag:%s, setting_name:%s): %s" + % (source_file, e.line_no, tag, setting_name, str(e)) + ) + except Exception as e: + raise Exception( + "Error parsing %s (tag:%s, setting_name:%s): %s" + % (source_file, tag, setting_name, str(e)) + ) + + ET.ElementTree(root_node).write(output) + + +def main(): + if len(sys.argv) < 4: + print("Usage: %s [tag] [output-xml-file] [srcfiles...]" % (sys.argv[0])) + exit(1) + + process_settings_docs( + tag=sys.argv[1], output=sys.argv[2], source_files=sys.argv[3:] + ) + + +if __name__ == "__main__": + main() + + +############################################################################### +# Tests +############################################################################### + + +def setup_module(): + global pytest + import pytest + + +def t_srcdir(): + return os.path.abspath(os.path.dirname(__file__) + "/..") + + +def t_setting_c(name): + return t_srcdir() + f"/src/libnm-core-impl/nm-setting-{name}.c" + + +def test_file_location(): + assert t_srcdir() + "/tools/generate-docs-nm-property-infos.py" == os.path.abspath( + __file__ + ) + assert os.path.isfile(t_srcdir() + "/src/libnm-core-impl/nm-setting-connection.c") + + assert os.path.isfile(t_setting_c("ip-config")) + + +def test_get_setting_names(): + assert "connection" == get_setting_names( + t_srcdir() + "/src/libnm-core-impl/nm-setting-connection.c" + ) + assert "ipv4" == get_setting_names( + t_srcdir() + "/src/libnm-core-impl/nm-setting-ip4-config.c" + ) + assert None == get_setting_names( + t_srcdir() + "/src/libnm-core-impl/nm-setting-ip-config.c" + ) + + +def test_get_file_infos(): + + t = ["connection", "ip-config", "ip4-config", "proxy", "wired"] + + assert [ + ( + "connection", + t_setting_c("connection"), + ), + ( + "ipv4", + t_setting_c("ip4-config"), + ), + ("proxy", t_setting_c("proxy")), + ( + "802-3-ethernet", + t_setting_c("wired"), + ), + ] == list(get_file_infos([t_setting_c(x) for x in t])) + + +def test_process_setting(): + root_node = ET.Element("nm-setting-docs") + process_setting("nmcli", root_node, t_setting_c("connection"), "connection") From 8899ecc0d84744da19b5e1a686b89443d89ea42f Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Tue, 4 Oct 2022 12:12:51 +0200 Subject: [PATCH 3/6] tools: preserve newlines and indentation in "generate-docs-nm-property-infos.py" Our docs can be long. It's important to be able to express paragraphs. Honor a blank line to include a newline. For XML often whitespace is ignored, but our tools can choose to honor the newline. Also, don't strip the whitespace from the beginning and the end. We keep whitespace for a certain indentation level, but additional whitespace gets preserved. This is less important, because regular spaces is indeed irrelevant. But when we write the annotations, we should be in full control over spaces. --- src/libnmc-setting/settings-docs.h.in | 2 +- src/nmcli/generate-docs-nm-settings-nmcli.xml.in | 2 +- tools/generate-docs-nm-property-infos.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libnmc-setting/settings-docs.h.in b/src/libnmc-setting/settings-docs.h.in index 4df96c7d31..cf87f38c15 100644 --- a/src/libnmc-setting/settings-docs.h.in +++ b/src/libnmc-setting/settings-docs.h.in @@ -24,7 +24,7 @@ #define DESCRIBE_DOC_NM_SETTING_CONNECTION_STABLE_ID N_("This represents the identity of the connection used for various purposes. It allows to configure multiple profiles to share the identity. Also, the stable-id can contain placeholders that are substituted dynamically and deterministically depending on the context. The stable-id is used for generating IPv6 stable private addresses with ipv6.addr-gen-mode=stable-privacy. It is also used to seed the generated cloned MAC address for ethernet.cloned-mac-address=stable and wifi.cloned-mac-address=stable. It is also used as DHCP client identifier with ipv4.dhcp-client-id=stable and to derive the DHCP DUID with ipv6.dhcp-duid=stable-[llt,ll,uuid]. Note that depending on the context where it is used, other parameters are also seeded into the generation algorithm. For example, a per-host key is commonly also included, so that different systems end up generating different IDs. Or with ipv6.addr-gen-mode=stable-privacy, also the device's name is included, so that different interfaces yield different addresses. The per-host key is the identity of your machine and stored in /var/lib/NetworkManager/secret_key. See NetworkManager(8) manual about the secret-key and the host identity. The '$' character is treated special to perform dynamic substitutions at runtime. Currently, supported are \"${CONNECTION}\", \"${DEVICE}\", \"${MAC}\", \"${BOOT}\", \"${RANDOM}\". These effectively create unique IDs per-connection, per-device, per-boot, or every time. Note that \"${DEVICE}\" corresponds to the interface name of the device and \"${MAC}\" is the permanent MAC address of the device. Any unrecognized patterns following '$' are treated verbatim, however are reserved for future use. You are thus advised to avoid '$' or escape it as \"$$\". For example, set it to \"${CONNECTION}-${BOOT}-${DEVICE}\" to create a unique id for this connection that changes with every reboot and differs depending on the interface where the profile activates. If the value is unset, a global connection default is consulted. If the value is still unset, the default is similar to \"${CONNECTION}\" and uses a unique, fixed ID for the connection.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_TIMESTAMP N_("The time, in seconds since the Unix Epoch, that the connection was last _successfully_ fully activated. NetworkManager updates the connection timestamp periodically when the connection is active to ensure that an active connection has the latest timestamp. The property is only meant for reading (changes to this property will not be preserved).") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_TYPE N_("Base type of the connection. For hardware-dependent connections, should contain the setting name of the hardware-type specific setting (ie, \"802-3-ethernet\" or \"802-11-wireless\" or \"bluetooth\", etc), and for non-hardware dependent connections like VPN or otherwise, should contain the setting name of that setting type (ie, \"vpn\" or \"bridge\", etc).") -#define DESCRIBE_DOC_NM_SETTING_CONNECTION_UUID N_("The connection.uuid is the real identifier of a profile. It cannot change and it must be unique. It is therefore often best to refer to a profile by UUID, for example with `nmcli connection up uuid $UUID`. The UUID cannot be changed, except in offline mode. In that case, the special values \"new\", \"generate\" and \"\" are allowed to generate a new random UUID.") +#define DESCRIBE_DOC_NM_SETTING_CONNECTION_UUID N_("The connection.uuid is the real identifier of a profile. It cannot change and it must be unique. It is therefore often best to refer to a profile by UUID, for example with `nmcli connection up uuid $UUID`. The UUID cannot be changed, except in offline mode. In that case, the special values \"new\", \"generate\" and \"\" are allowed to generate a new random UUID.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_WAIT_ACTIVATION_DELAY N_("Time in milliseconds to wait for connection to be considered activated. The wait will start after the pre-up dispatcher event. The value 0 means no wait time. The default value is -1, which currently has the same meaning as no wait time.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_WAIT_DEVICE_TIMEOUT N_("Timeout in milliseconds to wait for device at startup. During boot, devices may take a while to be detected by the driver. This property will cause to delay NetworkManager-wait-online.service and nm-online to give the device a chance to appear. This works by waiting for the given timeout until a compatible device for the profile is available and managed. The value 0 means no wait time. The default value is -1, which currently has the same meaning as no wait time.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_ZONE N_("The trust level of a the connection. Free form case-insensitive string (for example \"Home\", \"Work\", \"Public\"). NULL or unspecified zone means the connection will be placed in the default zone as defined by the firewall. When updating this property on a currently activated connection, the change takes effect immediately.") diff --git a/src/nmcli/generate-docs-nm-settings-nmcli.xml.in b/src/nmcli/generate-docs-nm-settings-nmcli.xml.in index c3cb530096..464d53b9ab 100644 --- a/src/nmcli/generate-docs-nm-settings-nmcli.xml.in +++ b/src/nmcli/generate-docs-nm-settings-nmcli.xml.in @@ -369,7 +369,7 @@ alias="con-name" description="A human readable unique identifier for the connection, like "Work Wi-Fi" or "T-Mobile 3G"." /> + description="The connection.uuid is the real identifier of a profile. It cannot change and it must be unique. It is therefore often best to refer to a profile by UUID, for example with `nmcli connection up uuid $UUID`. The UUID cannot be changed, except in offline mode. In that case, the special values "new", "generate" and "" are allowed to generate a new random UUID." /> Date: Tue, 4 Oct 2022 10:16:07 +0200 Subject: [PATCH 4/6] tools: don't write empty XML nodes in "generate-docs-nm-property-infos.py" This generates '\n' nodes in case there is no additional data. Don't do that. Just '' --- tools/generate-docs-nm-property-infos.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/generate-docs-nm-property-infos.py b/tools/generate-docs-nm-property-infos.py index 93391c2e57..aab4208352 100755 --- a/tools/generate-docs-nm-property-infos.py +++ b/tools/generate-docs-nm-property-infos.py @@ -246,8 +246,6 @@ def process_setting(tag, root_node, source_file, setting_name): end_tag = "---end---" setting_node, created = xnode_get_or_create(root_node, "setting", setting_name) - if created: - setting_node.text = "\n" try: f = open(source_file, "r") From c1bebdfaa6994a053a0905c9348009ae5bc3de8c Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Tue, 4 Oct 2022 10:16:07 +0200 Subject: [PATCH 5/6] tools: don't set empty attributes in "generate-docs-nm-property-infos.py" If the information is missing, the entire attribute should not be there. Don't set it to the empty word. Also, don't alias the "variable" attribute to the "name". It's not clear what the "variable" fields is supposed to mean, but if it's not explicitly set, don't make up the information. If a user of that information cares, the can always fallback to the "name". --- tools/generate-docs-nm-property-infos.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tools/generate-docs-nm-property-infos.py b/tools/generate-docs-nm-property-infos.py index aab4208352..ce30100e49 100755 --- a/tools/generate-docs-nm-property-infos.py +++ b/tools/generate-docs-nm-property-infos.py @@ -132,12 +132,7 @@ def write_data(tag, setting_node, line_no, parsed_data): v = parsed_data.get(k, None) if v is None: - if k == "variable": - v = name - elif k == 'description-docbook': - continue - else: - v = "" + continue if xmltype == KEYWORD_XML_TYPE_NESTED: # Set as XML nodes. The input data is XML itself. From 77e0041b274826d17ed94a1680a045aec756819d Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Tue, 4 Oct 2022 10:15:17 +0200 Subject: [PATCH 6/6] tools: sort the settings in "generate-docs-nm-property-infos.py" by name "nm-setting-ip-config.c" is a base class for IPv4 and IPv6 settings. So far, any tags there were ignored, which was not obvious. It can be useful to document common properties there. Well, maybe every property better has a IPv4/IPv6 specific text, but that should not be a technical limitation of the tool. So also honor the base file for "ipv4" and "ipv6" settings. When doing that, the settings are no longer processed in the order as they are provided on the command line. Because, one file would be parsed twice, it wouldn't make much sense. Instead, sort the my setting name. The advantage is that the generated XML is independent from the order that make/meson passes to the tool. --- tools/generate-docs-nm-property-infos.py | 55 +++++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/tools/generate-docs-nm-property-infos.py b/tools/generate-docs-nm-property-infos.py index ce30100e49..dd40531e70 100755 --- a/tools/generate-docs-nm-property-infos.py +++ b/tools/generate-docs-nm-property-infos.py @@ -60,7 +60,7 @@ def get_setting_names(source_file): if file_base == "nm-setting-ip-config": # Special case ip-config, which is a base class. - return None + return 0, ("ipv4", "ipv6") header_file = "%s/libnm-core-public/%s.h" % (path_prefix, file_base) @@ -75,7 +75,7 @@ def get_setting_names(source_file): for line in f: m = re.search(r"^#define +NM_SETTING_.+SETTING_NAME\s+\"(\S+)\"$", line) if m: - return m.group(1) + return 1, (m.group(1),) raise Exception( 'Can\'t find setting name in header file "%s" for "%s"' @@ -84,10 +84,35 @@ def get_setting_names(source_file): def get_file_infos(source_files): + # This function parses the source files and detects the + # used setting name. The returned sections are sorted by setting + # name. + # + # The file "nm-setting-ip-config.c" can contain information + # for "ipv4" and "ipv6" settings. Thus, to sort the files + # is a bit more involved. + + # First, get a list of priority and setting-names that belong + # to the source file. Sort by priority,setting-names. It's + # important that "nm-setting-ip-config.c" gets parsed before + # "nm-setting-ip[46]-config.c". + file_infos = [] for source_file in source_files: - setting_name = get_setting_names(source_file) - if setting_name: - yield setting_name, source_file + priority, setting_names = get_setting_names(source_file) + file_infos.append((priority, setting_names, source_file)) + file_infos.sort() + + d = {} + for priority, setting_names, source_file in file_infos: + for setting_name in setting_names: + l = d.get(setting_name, None) + if l is None: + l = list() + d[setting_name] = l + l.append(source_file) + for key in sorted(d.keys()): + for f in d[key]: + yield key, f KEYWORD_XML_TYPE_NESTED = "nested" @@ -368,13 +393,13 @@ def test_file_location(): def test_get_setting_names(): - assert "connection" == get_setting_names( + assert (1, ("connection",)) == get_setting_names( t_srcdir() + "/src/libnm-core-impl/nm-setting-connection.c" ) - assert "ipv4" == get_setting_names( + assert (1, ("ipv4",)) == get_setting_names( t_srcdir() + "/src/libnm-core-impl/nm-setting-ip4-config.c" ) - assert None == get_setting_names( + assert (0, ("ipv4", "ipv6")) == get_setting_names( t_srcdir() + "/src/libnm-core-impl/nm-setting-ip-config.c" ) @@ -384,19 +409,27 @@ def test_get_file_infos(): t = ["connection", "ip-config", "ip4-config", "proxy", "wired"] assert [ + ( + "802-3-ethernet", + t_setting_c("wired"), + ), ( "connection", t_setting_c("connection"), ), + ( + "ipv4", + t_setting_c("ip-config"), + ), ( "ipv4", t_setting_c("ip4-config"), ), - ("proxy", t_setting_c("proxy")), ( - "802-3-ethernet", - t_setting_c("wired"), + "ipv6", + t_setting_c("ip-config"), ), + ("proxy", t_setting_c("proxy")), ] == list(get_file_infos([t_setting_c(x) for x in t]))