diff options
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | interface-definitions/include/interface-mtu-1200-9000.xml.i | 13 | ||||
-rw-r--r-- | interface-definitions/interfaces-vxlan.xml.in | 2 | ||||
-rw-r--r-- | interface-definitions/snmp.xml.in | 24 | ||||
-rw-r--r-- | interface-definitions/vrrp.xml.in | 8 | ||||
-rw-r--r-- | op-mode-definitions/pppoe-server.xml | 30 | ||||
-rw-r--r-- | python/vyos/defaults.py | 2 | ||||
-rw-r--r-- | python/vyos/keepalived.py | 31 | ||||
-rw-r--r-- | python/vyos/systemversions.py | 28 | ||||
-rw-r--r-- | schema/interface_definition.rnc | 17 | ||||
-rw-r--r-- | schema/interface_definition.rng | 18 | ||||
-rwxr-xr-x | scripts/build-command-templates | 2 | ||||
-rwxr-xr-x | scripts/build-component-versions | 47 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/ipsec-settings.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/system-syslog.py | 13 | ||||
-rwxr-xr-x | src/conf_mode/vrrp.py | 6 | ||||
-rwxr-xr-x | src/op_mode/powerctrl.py | 38 | ||||
-rwxr-xr-x | src/op_mode/vrrp.py | 3 | ||||
-rwxr-xr-x | src/system/vrrp-script-wrapper.py | 49 |
20 files changed, 263 insertions, 87 deletions
@@ -1,6 +1,7 @@ TMPL_DIR := templates-cfg OP_TMPL_DIR := templates-op BUILD_DIR := build +DATA_DIR := data CFLAGS := src = $(wildcard interface-definitions/*.xml.in) @@ -77,8 +78,13 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/reset/vpn/node.def rm -f $(OP_TMPL_DIR)/show/system/node.def +.PHONY: component_versions +.ONESHELL: +component_versions: $(BUILD_DIR) $(obj) + $(CURDIR)/scripts/build-component-versions $(BUILD_DIR)/interface-definitions $(DATA_DIR) + .PHONY: all -all: clean interface_definitions op_mode_definitions +all: clean interface_definitions op_mode_definitions component_versions .PHONY: clean clean: diff --git a/interface-definitions/include/interface-mtu-1200-9000.xml.i b/interface-definitions/include/interface-mtu-1200-9000.xml.i new file mode 100644 index 000000000..336845b77 --- /dev/null +++ b/interface-definitions/include/interface-mtu-1200-9000.xml.i @@ -0,0 +1,13 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>1200-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1200-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 1200 and 9000</constraintErrorMessage> + </properties> +</leafNode> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index ceb75cb34..7d86e1847 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -57,7 +57,7 @@ </completionHelp> </properties> </leafNode> - #include <include/interface-mtu-1450-9000.xml.i> + #include <include/interface-mtu-1200-9000.xml.i> <leafNode name="remote"> <properties> <help>Remote address of VXLAN tunnel</help> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index 91f1b8d71..4c6a993b2 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -139,6 +139,14 @@ <leafNode name="trap-source"> <properties> <help>SNMP trap source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> @@ -148,6 +156,14 @@ <tagNode name="trap-target"> <properties> <help>Address of trap target</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> @@ -585,6 +601,10 @@ <tagNode name="extension-name"> <properties> <help>Extension name</help> + <constraint> + <regex>^[a-z0-9\.\-\_]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> </properties> <children> <leafNode name="script"> @@ -593,6 +613,10 @@ <completionHelp> <script>ls /config/user-data</script> </completionHelp> + <constraint> + <regex>^[a-z0-9\.\-\_\/]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> </properties> </leafNode> </children> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in index 2884ef613..89d22f79f 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/vrrp.xml.in @@ -197,6 +197,14 @@ </constraint> </properties> </leafNode> + <leafNode name="stop"> + <properties> + <help>Script to run on VRRP state transition to stop</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> </children> </node> <leafNode name="virtual-address"> diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml index 13a4570e8..0293c9502 100644 --- a/op-mode-definitions/pppoe-server.xml +++ b/op-mode-definitions/pppoe-server.xml @@ -71,4 +71,34 @@ </node> </children> </node> + <node name="set"> + <children> + <node name="pppoe-server"> + <properties> + <help>Set PPPoE server maintenance mode</help> + </properties> + <children> + <node name="maintenance-mode"> + <properties> + <help>Set PPPoE server maintenance mode</help> + </properties> + <children> + <leafNode name="enable"> + <properties> + <help>Deny new connections and stop to serve pppoe after disconnect last session</help> + </properties> + <command>/usr/bin/accel-cmd shutdown soft</command> + </leafNode> + <leafNode name="cancel"> + <properties> + <help>Cancel maintenance mode</help> + </properties> + <command>/usr/bin/accel-cmd shutdown cancel</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> </interfaceDefinition> diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index dedb929b4..a2ad142bc 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -29,6 +29,8 @@ cfg_vintage = 'vyatta' commit_lock = '/opt/vyatta/config/.lock' +version_file = '/usr/share/vyos/component-versions.json' + https_data = { 'listen_addresses' : { '*': ['_'] } } diff --git a/python/vyos/keepalived.py b/python/vyos/keepalived.py index 4114aa736..3984ca792 100644 --- a/python/vyos/keepalived.py +++ b/python/vyos/keepalived.py @@ -26,8 +26,6 @@ state_file = '/tmp/keepalived.data' stats_file = '/tmp/keepalived.stats' json_file = '/tmp/keepalived.json' -state_dir = '/var/run/vyos/vrrp/' - def vrrp_running(): if not os.path.exists(vyos.keepalived.pid_file) \ or not vyos.util.process_running(vyos.keepalived.pid_file): @@ -38,6 +36,15 @@ def vrrp_running(): def keepalived_running(): return vyos.util.process_running(pid_file) +## Clear VRRP data after showing +def remove_vrrp_data(data_file): + if data_file == "json" and os.path.exists(json_file): + os.remove(json_file) + elif data_file == "stats" and os.path.exists(stats_file): + os.remove(stats_file) + elif data_file == "state" and os.path.exists(state_file): + os.remove(state_file) + def force_state_data_dump(): pid = vyos.util.read_file(pid_file) os.kill(int(pid), signal.SIGUSR1) @@ -76,26 +83,6 @@ def decode_state(code): return state -## The functions are mainly for transition script wrappers -## to compensate for the fact that keepalived doesn't keep persistent -## state between reloads. -def get_old_state(group): - file = os.path.join(state_dir, "{0}.state".format(group)) - if os.path.exists(file): - with open(file, 'r') as f: - data = f.read().strip() - return data - else: - return None - -def save_state(group, state): - if not os.path.exists(state_dir): - os.makedirs(state_dir) - - file = os.path.join(state_dir, "{0}.state".format(group)) - with open(file, 'w') as f: - f.write(state) - ## These functions are for the old, and hopefully obsolete plaintext ## (non machine-readable) data format introduced by Vyatta back in the days ## They are kept here just in case, if JSON output option turns out or becomes diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py index 9b3f4f413..5c4deca29 100644 --- a/python/vyos/systemversions.py +++ b/python/vyos/systemversions.py @@ -16,12 +16,15 @@ import os import re import sys +import json + import vyos.defaults def get_system_versions(): """ - Get component versions from running system; critical failure if - unable to read migration directory. + Get component versions from running system: read vyatta directory + structure for versions, then read vyos JSON file. It is a critical + error if either migration directory or JSON file is unreadable. """ system_versions = {} @@ -36,4 +39,25 @@ def get_system_versions(): pair = info.split('@') system_versions[pair[0]] = int(pair[1]) + version_dict = {} + path = vyos.defaults.version_file + + if os.path.isfile(path): + with open(path, 'r') as f: + try: + version_dict = json.load(f) + except ValueError as err: + print(f"\nValue error in {path}: {err}") + sys.exit(1) + + for k, v in version_dict.items(): + if not isinstance(v, int): + print(f"\nType error in {path}; expecting Dict[str, int]") + sys.exit(1) + existing = system_versions.get(k) + if existing is None: + system_versions[k] = v + elif v > existing: + system_versions[k] = v + return system_versions diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc index 02175fec8..0ce8226cd 100644 --- a/schema/interface_definition.rnc +++ b/schema/interface_definition.rnc @@ -24,9 +24,16 @@ # Interface definition starts with interfaceDefinition tag that may contain node tags start = element interfaceDefinition { + syntaxVersion*, node* } +# interfaceDefinition may contain syntax version attribute lists. +syntaxVersion = element syntaxVersion +{ + (componentAttr & versionAttr) +} + # node tag may contain node, leafNode, or tagNode tags # Those are intermediate configuration nodes that may only contain # other nodes and must not have values @@ -97,6 +104,16 @@ properties = element properties (element keepChildOrder { empty })? } +componentAttr = attribute component +{ + text +} + +versionAttr = attribute version +{ + text +} + # All nodes must have "name" attribute nodeNameAttr = attribute name { diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng index 195ef27f4..bfd8d376f 100644 --- a/schema/interface_definition.rng +++ b/schema/interface_definition.rng @@ -29,10 +29,22 @@ <start> <element name="interfaceDefinition"> <zeroOrMore> + <ref name="syntaxVersion"/> + </zeroOrMore> + <zeroOrMore> <ref name="node"/> </zeroOrMore> </element> </start> + <!-- interfaceDefinition may contain syntax version attribute lists. --> + <define name="syntaxVersion"> + <element name="syntaxVersion"> + <interleave> + <ref name="componentAttr"/> + <ref name="versionAttr"/> + </interleave> + </element> + </define> <!-- node tag may contain node, leafNode, or tagNode tags Those are intermediate configuration nodes that may only contain @@ -184,6 +196,12 @@ </interleave> </element> </define> + <define name="componentAttr"> + <attribute name="component"/> + </define> + <define name="versionAttr"> + <attribute name="version"/> + </define> <!-- All nodes must have "name" attribute --> <define name="nodeNameAttr"> <attribute name="name"/> diff --git a/scripts/build-command-templates b/scripts/build-command-templates index 4fcdb8ade..dbf4ad9c5 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -295,4 +295,6 @@ root = xml.getroot() nodes = root.iterfind("*") for n in nodes: + if n.tag == "syntaxVersion": + continue process_node(n, [output_dir]) diff --git a/scripts/build-component-versions b/scripts/build-component-versions new file mode 100755 index 000000000..5362dbdd4 --- /dev/null +++ b/scripts/build-component-versions @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import sys +import os +import argparse +import json + +from lxml import etree as ET + +parser = argparse.ArgumentParser() +parser.add_argument('INPUT_DIR', type=str, + help="Directory containing XML interface definition files") +parser.add_argument('OUTPUT_DIR', type=str, + help="Output directory for JSON file") + +args = parser.parse_args() + +input_dir = args.INPUT_DIR +output_dir = args.OUTPUT_DIR + +version_dict = {} + +for filename in os.listdir(input_dir): + filepath = os.path.join(input_dir, filename) + print(filepath) + try: + xml = ET.parse(filepath) + except Exception as e: + print("Failed to load interface definition file {0}".format(filename)) + print(e) + sys.exit(1) + + root = xml.getroot() + version_data = root.iterfind("syntaxVersion") + for ver in version_data: + component = ver.get("component") + version = int(ver.get("version")) + + v = version_dict.get(component) + if v is None: + version_dict[component] = version + elif version > v: + version_dict[component] = version + +out_file = os.path.join(output_dir, 'component-versions.json') +with open(out_file, 'w') as f: + json.dump(version_dict, f, indent=4, sort_keys=True) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 7f1ac6c31..efdc21f89 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -189,13 +189,13 @@ def apply(vxlan): # configure ARP cache timeout in milliseconds v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) # configure ARP filter configuration - v.set_arp_filter(bond['ip_disable_arp_filter']) + v.set_arp_filter(vxlan['ip_disable_arp_filter']) # configure ARP accept - v.set_arp_accept(bond['ip_enable_arp_accept']) + v.set_arp_accept(vxlan['ip_enable_arp_accept']) # configure ARP announce - v.set_arp_announce(bond['ip_enable_arp_announce']) + v.set_arp_announce(vxlan['ip_enable_arp_announce']) # configure ARP ignore - v.set_arp_ignore(bond['ip_enable_arp_ignore']) + v.set_arp_ignore(vxlan['ip_enable_arp_ignore']) # Enable proxy-arp on this interface v.set_proxy_arp(vxlan['ip_proxy_arp']) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index aab3e9734..e80c6caf0 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -248,7 +248,8 @@ def generate(data): write_ipsec_ra_conn(data) append_ipsec_conf(data) else: - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) + if os.path.exists(ipsec_ra_conn_file): + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_flie) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 15533afab..2d47cc061 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -19,6 +19,7 @@ import sys import os import re +import subprocess import jinja2 from vyos.config import Config @@ -313,15 +314,11 @@ def verify(c): def apply(c): - if not c and os.path.exists('/var/run/rsyslogd.pid'): - os.system("sudo systemctl stop syslog.socket") - os.system("sudo systemctl stop rsyslog") - else: - if not os.path.exists('/var/run/rsyslogd.pid'): - os.system("sudo systemctl start rsyslog >/dev/null") - else: - os.system("sudo systemctl restart rsyslog >/dev/null") + if not c: + subprocess.call(['sudo', 'systemctl', 'stop', 'syslog']) + return 0 + subprocess.call(['sudo', 'systemctl', 'restart', 'syslog']) if __name__ == '__main__': try: diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d31be4cfb..1d8477769 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -37,6 +37,7 @@ config_tmpl = """ global_defs { dynamic_interfaces + script_user root } {% for group in groups -%} @@ -117,6 +118,10 @@ vrrp_instance {{ group.name }} { {% if group.fault_script -%} notify_fault "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state fault --group {{ group.name }} --interface {{ group.interface }} {{ group.fault_script }}" {% endif -%} + + {% if group.stop_script -%} + notify_stop "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state stop --group {{ group.name }} --interface {{ group.interface }} {{ group.stop_script }}" + {% endif -%} } {% endfor -%} @@ -178,6 +183,7 @@ def get_config(): group["master_script"] = config.return_value("transition-script master") group["backup_script"] = config.return_value("transition-script backup") group["fault_script"] = config.return_value("transition-script fault") + group["stop_script"] = config.return_value("transition-script stop") if config.exists("no-preempt"): group["preempt"] = False diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index a6188ec74..54fc12be3 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -24,6 +24,7 @@ from datetime import datetime, timedelta, time as type_time, date as type_date from subprocess import check_output, CalledProcessError, STDOUT from vyos.util import ask_yes_no +systemd_sched_file = "/run/systemd/shutdown/scheduled" def parse_time(s): try: @@ -45,33 +46,40 @@ def parse_date(s): def get_shutdown_status(): try: - output = check_output(["/bin/systemctl", "status", "systemd-shutdownd.service"]).decode() - return output + if os.path.exists(systemd_sched_file): + # Get scheduled from systemd file + with open(systemd_sched_file, 'r') as f: + data = f.read().rstrip('\n') + r_data = {} + for line in data.splitlines(): + tmp_split = line.split("=") + if tmp_split[0] == "USEC": + # Convert USEC to human readable format + r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') + else: + r_data[tmp_split[0]] = tmp_split[1] + return r_data + return None except CalledProcessError: return None def check_shutdown(): output = get_shutdown_status() - if output: - r = re.findall(r'Status: \"(.*)\"\n', output) - if r: - # When available, that line is like - # Status: "Shutting down at Thu 1970-01-01 00:00:00 UTC (poweroff)..." - print(r[0]) - else: - # Sometimes status string is not available immediately - # after service startup - print("Poweroff or reboot is scheduled") + if output and 'MODE' in output: + if output['MODE'] == 'reboot': + print("Reboot is scheduled", output['DATETIME']) + elif output['MODE'] == 'poweroff': + print("Poweroff is scheduled", output['DATETIME']) else: - print("Poweroff or reboot is not scheduled") + print("Reboot or poweroff is not scheduled") def cancel_shutdown(): output = get_shutdown_status() - if output: + if output and 'MODE' in output: try: timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') cmd = check_output(["/sbin/shutdown","-c","--no-wall"]) - message = "Scheduled reboot or poweroff has been cancelled %s" % timenow + message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow) os.system("wall %s" % message) except CalledProcessError as e: sys.exit("Could not cancel a reboot or poweroff: %s" % e) diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 54e1bfb57..8d1369823 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -32,6 +32,7 @@ def print_summary(): # Replace with inotify or similar if it proves problematic time.sleep(0.2) json_data = vyos.keepalived.get_json_data() + vyos.keepalived.remove_vrrp_data("json") except: print("VRRP information is not available") sys.exit(1) @@ -63,6 +64,7 @@ def print_statistics(): time.sleep(0.2) output = vyos.keepalived.get_statistics() print(output) + vyos.keepalived.remove_vrrp_data("stats") except: print("VRRP statistics are not available") sys.exit(1) @@ -73,6 +75,7 @@ def print_state_data(): time.sleep(0.2) output = vyos.keepalived.get_state_data() print(output) + vyos.keepalived.remove_vrrp_data("state") except: print("VRRP information is not available") sys.exit(1) diff --git a/src/system/vrrp-script-wrapper.py b/src/system/vrrp-script-wrapper.py index ccd640128..c28ecba55 100755 --- a/src/system/vrrp-script-wrapper.py +++ b/src/system/vrrp-script-wrapper.py @@ -23,7 +23,6 @@ import argparse import syslog import vyos.util -import vyos.keepalived parser = argparse.ArgumentParser() @@ -44,38 +43,22 @@ if not args.script or not args.state or not args.group \ # to pass arguments to the script args.script = " ".join(args.script) -# Get the old state if it exists and compare it to the current state received -# in command line options to avoid executing scripts if no real transition occured. -# This is necessary because keepalived does not keep persistent state data even between -# config reloads and will cheerfully execute everything whether it's required or not. - -old_state = vyos.keepalived.get_old_state(args.group) - -if (old_state is None) or (old_state != args.state): - exitcode = 0 - - # Run the script and save the new state - - # Change the process GID to the config owners group to avoid screwing up - # running config permissions - os.setgid(vyos.util.get_cfg_group_id()) - - syslog.syslog(syslog.LOG_NOTICE, 'Running transition script {0} for VRRP group {1}'.format(args.script, args.group)) - try: - ret = subprocess.call("%s %s %s %s" % ( args.script, args.state, args.interface, args.group), shell=True) - if ret != 0: - syslog.syslog(syslog.LOG_ERR, "Transition script {0} failed, exit status: {1}".format(args.script, ret)) - exitcode = ret - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "Failed to execute transition script {0}: {1}".format(args.script, e)) - exitcode = 1 - - if exitcode == 0: - syslog.syslog(syslog.LOG_NOTICE, "Transition script {0} executed successfully".format(args.script)) - - vyos.keepalived.save_state(args.group, args.state) -else: - syslog.syslog(syslog.LOG_NOTICE, "State of the group {0} has not changed, not running transition script".format(args.group)) +exitcode = 0 +# Change the process GID to the config owners group to avoid screwing up +# running config permissions +os.setgid(vyos.util.get_cfg_group_id()) +syslog.syslog(syslog.LOG_NOTICE, 'Running transition script {0} for VRRP group {1}'.format(args.script, args.group)) +try: + ret = subprocess.call("%s %s %s %s" % ( args.script, args.state, args.interface, args.group), shell=True) + if ret != 0: + syslog.syslog(syslog.LOG_ERR, "Transition script {0} failed, exit status: {1}".format(args.script, ret)) + exitcode = ret +except Exception as e: + syslog.syslog(syslog.LOG_ERR, "Failed to execute transition script {0}: {1}".format(args.script, e)) + exitcode = 1 + +if exitcode == 0: + syslog.syslog(syslog.LOG_NOTICE, "Transition script {0} executed successfully".format(args.script)) syslog.closelog() sys.exit(exitcode) |