diff --git a/src/apps/config/leader.lua b/src/apps/config/leader.lua index ceb9457433..4d7f1d2597 100644 --- a/src/apps/config/leader.lua +++ b/src/apps/config/leader.lua @@ -186,6 +186,18 @@ function Leader:rpc_set_alarm_operator_state (args) if success then return response else return {status=1, error=response} end end +function Leader:rpc_purge_alarms (args) + local function purge() + if args.schema ~= self.schema_name then + return false, ("Purge-alarms operation not supported in".. + "'%s' schema"):format(args.schema) + end + return { purged_alarms = alarms.purge_alarms(args) } + end + local success, response = pcall(purge) + if success then return response else return {status=1, error=response} end +end + local function path_parser_for_grammar(grammar, path) local getter, subgrammar = path_mod.resolver(grammar, path) return data.data_parser_from_grammar(subgrammar) diff --git a/src/lib/yang/alarms.lua b/src/lib/yang/alarms.lua index 885df3ae29..ffa5b0935d 100644 --- a/src/lib/yang/alarms.lua +++ b/src/lib/yang/alarms.lua @@ -6,6 +6,7 @@ local util = require('lib.yang.util') local alarm_codec = require('apps.config.alarm_codec') local format_date_as_iso_8601 = util.format_date_as_iso_8601 +local parse_date_as_iso_8601 = util.parse_date_as_iso_8601 local state = { alarm_inventory = { @@ -274,6 +275,141 @@ function set_operator_state (key, args) return true end +-- Purge alarms. + +local ages = {seconds=1, minutes=60, hours=3600, days=3600*24, weeks=3600*24*7} + +local function toseconds (date) + if type(date) == 'table' then + assert(date.age_spec and date.value, "Not a valid 'older_than' data type") + + local multiplier = assert(ages[date.age_spec], + "Not a valid 'age_spec' value: "..date.age_spec) + return date.value * multiplier + elseif type(date) == 'string' then + local t = {} + t.year, t.month, t.day, t.hour, t.min, t.sec = parse_date_as_iso_8601(date) + return os.time(t) + else + error('Wrong data type: '..type(date)) + end +end + +-- `purge_alarms` requests the server to delete entries from the alarm list +-- according to the supplied criteria. Typically it can be used to delete +-- alarms that are in closed operator state and older than a specified time. +-- The number of purged alarms is returned as an output parameter. +-- +-- args: {status, older_than, severity, operator_state} +function purge_alarms (args) + local alarm_list = state.alarm_list + local alarms = state.alarm_list.alarm + args.alarm_status = args.alarm_status or 'any' + local function purge_alarm (key) + alarms[key] = nil + alarm_list.number_of_alarms = alarm_list.number_of_alarms - 1 + end + local function by_status (alarm, args) + local status = assert(args.alarm_status) + local alarm_statuses = lib.set('any', 'cleared', 'not-cleared') + assert(alarm_statuses[status], 'Not a valid status value: '..status) + if status == 'any' then return true end + if status == 'cleared' then return alarm.is_cleared end + if status == 'not-cleared' then return not alarm.is_cleared end + return false + end + local function by_older_than (alarm, args) + local older_than = assert(args.older_than) + if type(older_than) == 'string' then + local age_spec, value = older_than:match("([%w]+):([%d]+)") + older_than = {value = value, age_spec = age_spec} + end + assert(type(older_than) == 'table') + local alarm_time = toseconds(alarm.time_created) + local threshold = toseconds(older_than) + return util.gmtime() - alarm_time >= threshold + end + local function by_severity (alarm, args) + local severity = assert(args.severity) + if type(severity) == 'string' then + local sev_spec, value = severity:match("([%w]+):([%w]+)") + severity = {sev_spec = sev_spec, value = value} + end + assert(type(severity) == 'table' and severity.sev_spec and severity.value, + 'Not valid severity data type') + local severities = {indeterminate=2, minor=3 , warning=4, major=5, critical=6} + local function tonumber (severity) + return severities[severity] + end + local sev_spec, severity = severity.sev_spec, tonumber(severity.value) + local alarm_severity = tonumber(alarm.perceived_severity) + if sev_spec == 'below' then + return alarm_severity < severity + elseif sev_spec == 'is' then + return alarm_severity == severity + elseif sev_spec == 'above' then + return alarm_severity > severity + else + error('Not valid sev-spec value: '..sev_spec) + end + return false + end + local function by_operator_state (alarm, args) + local operator_state = assert(args.operator_state_filter) + local state, user + if type(operator_state) == 'string' then + state, user = operator_state:match("([%w]+):([%w]+)") + if not state then + state, user = operator_state, operator_state + end + operator_state = {state=state, user=user} + end + assert(type(operator_state) == 'table') + local function tonumber (state) + return operator_states[state] + end + state, user = operator_state.state, operator_state.user + if state or user then + for _, state_change in pairs(alarm.operator_state_change or {}) do + if state and tonumber(state_change.state) == tonumber(state) then + return true + elseif user and state_change.user == user then + return true + end + end + end + return false + end + local args_to_filters = { older_than=by_older_than, + severity = by_severity, + operator_state_filter = by_operator_state, } + local filter = {} + function filter:initialize (args) + self.args = args + self.filters = { by_status } + for name, filter in pairs(args_to_filters) do + if args[name] then + table.insert(self.filters, filter) + end + end + end + function filter:apply (alarm) + for _, filter in ipairs(self.filters) do + if not filter(alarm, self.args) then return false end + end + return true + end + local count = 0 + filter:initialize(args) + for key, alarm in pairs(alarms) do + if filter:apply(alarm) then + purge_alarm(key) + count = count + 1 + end + end + return count +end + -- function selftest () @@ -292,6 +428,7 @@ function selftest () -- Check alarm inventory has been loaded. require("apps.ipv4.arp") + require("apps.lwaftr.ndp") assert(table_size(state.alarm_inventory.alarm_type) > 0) -- Check number of alarms is zero. @@ -376,5 +513,47 @@ function selftest () local success = pcall(set_operator_state, key, {state='ack'}) assert(not success) + -- Test toseconds. + assert(toseconds({age_spec='weeks', value=1}) == 3600*24*7) + assert(toseconds('1970-01-01T00:00:00Z') == 0) + + -- Purge alarms by status. + assert(table_size(state.alarm_list.alarm) == 1) + assert(purge_alarms({alarm_status = 'any'}) == 1) + assert(table_size(state.alarm_list.alarm) == 0) + assert(purge_alarms({alarm_status = 'any'}) == 0) + + -- Purge alarms filtering by older_than. + local key = alarm_keys:fetch('external-interface', 'arp-resolution') + raise_alarm(key) + sleep(1) + assert(purge_alarms({older_than={age_spec='seconds', value='1'}}) == 1) + + -- Purge alarms by severity. + local key = alarm_keys:fetch('external-interface', 'arp-resolution') + raise_alarm(key) + assert(table_size(state.alarm_list.alarm) == 1) + assert(purge_alarms({severity={sev_spec='is', value='minor'}}) == 0) + assert(purge_alarms({severity={sev_spec='below', value='minor'}}) == 0) + assert(purge_alarms({severity={sev_spec='above', value='minor'}}) == 1) + + raise_alarm(key, {perceived_severity='minor'}) + assert(purge_alarms({severity={sev_spec='is', value='minor'}}) == 1) + + raise_alarm(alarm_keys:fetch('external-interface', 'arp-resolution')) + raise_alarm(alarm_keys:fetch('internal-interface', 'ndp-resolution')) + assert(table_size(state.alarm_list.alarm) == 2) + assert(purge_alarms({severity={sev_spec='above', value='minor'}}) == 2) + + -- Purge alarms by operator_state_filter. + local key = alarm_keys:fetch('external-interface', 'arp-resolution') + raise_alarm(key) + assert(table_size(state.alarm_list.alarm) == 1) + local success = set_operator_state(key, {state='ack'}) + assert(success) + local alarm = assert(state.alarm_list.alarm[key]) + assert(table_size(alarm.operator_state_change) == 1) + assert(purge_alarms({operator_state_filter={state='ack'}}) == 1) + print("ok") end diff --git a/src/lib/yang/snabb-config-leader-v1.yang b/src/lib/yang/snabb-config-leader-v1.yang index adab3f33b5..2bd4416930 100644 --- a/src/lib/yang/snabb-config-leader-v1.yang +++ b/src/lib/yang/snabb-config-leader-v1.yang @@ -128,4 +128,51 @@ module snabb-config-leader-v1 { leaf success { type boolean; description "True if operation succeeded."; } } } + + grouping filter-input { + description + "Grouping to specify a filter construct on alarm information."; + leaf alarm-status { + type string; + mandatory true; + description + "The clearance status of the alarm."; + } + leaf older-than { + type string; + description "Matches the 'last-status-change' leaf in the alarm."; + } + leaf severity { + type string; + description "Filter based on severity."; + } + leaf operator-state-filter { + type string; + description "Filter based on operator state."; + } + } + + rpc purge-alarms { + description + "This operation requests the server to delete entries from the + alarm list according to the supplied criteria. Typically it + can be used to delete alarms that are in closed operator state + and older than a specified time. The number of purged alarms + is returned as an output parameter"; + input { + leaf schema { type string; mandatory true; } + leaf revision { type string; } + leaf print-default { type boolean; } + leaf format { type string; } + uses filter-input; + } + output { + uses error-reporting; + leaf purged-alarms { + type uint32; + description "Number of purged alarms."; + } + } + } + } diff --git a/src/lib/yang/util.lua b/src/lib/yang/util.lua index 790235c624..7314b8f255 100644 --- a/src/lib/yang/util.lua +++ b/src/lib/yang/util.lua @@ -129,7 +129,7 @@ function memoize(f, max_occupancy) end end -local function gmtime () +function gmtime () local now = os.time() local utcdate = os.date("!*t", now) local localdate = os.date("*t", now) @@ -143,6 +143,14 @@ function format_date_as_iso_8601 (time) return os.date("%Y-%m-%dT%H:%M:%SZ", time) end +-- XXX: ISO 8601 can be more complex. We asumme date is the format returned +-- by 'format_date_as_iso8601'. +function parse_date_as_iso_8601 (date) + assert(type(date) == 'string') + local pattern = "(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)Z" + return assert(date:match(pattern)) +end + function selftest() print('selftest: lib.yang.util') assert(tointeger('0') == 0) diff --git a/src/program/config/README.md b/src/program/config/README.md index 57fc7d0748..f61f159c33 100644 --- a/src/program/config/README.md +++ b/src/program/config/README.md @@ -33,6 +33,9 @@ include: * [`snabb config set-alarm-operator-state`](./set_alarm_operator_state/README): add a new operator-state to an alarm +* [`snabb config purge-alarms`](./purge_alarms/README): + purge alarms by several criteria + The `snabb config get` et al commands are the normal way that Snabb users interact with Snabb applications in an ad-hoc fashion via the command line. `snabb config listen` is the standard way that a NETCONF diff --git a/src/program/config/purge_alarms/README b/src/program/config/purge_alarms/README new file mode 100644 index 0000000000..af9549697f --- /dev/null +++ b/src/program/config/purge_alarms/README @@ -0,0 +1,32 @@ +Usage: snabb config purge-alarms [OPTION]... ID STATE +Adds a new operator-state in an alarm. + +Available options: + -s, --schema SCHEMA YANG data interface to request. + -r, --revision REVISION Require a specific revision of the YANG module. + -f, --format Selects output format (yang or xpath). Default: yang. + --print-default Forces print out of default values. + -h, --help Displays this message. + +Filtering options: + --by-severity [severity-spec:value]. + Severity spec: 'is', 'below', 'above'. + Value: 'indeterminate', 'minor', 'warning', 'major', 'critical'. + --by-older-than [age-spec:value]. + Age spec: 'seconds', 'minutes', 'hours', 'days', 'weeks'. + Value: integer. + --by-operator-state-filter [state:user]. + State (optional): 'none', 'ack', 'closed', 'shelved', 'un-shelved'. + User (optional): string. + +Given an instance identifier and an alarm state ('any', 'cleared' or 'not-cleared') +remove all alarms that match the filtering options. + +Typical usage: + +$ sudo ./snabb config purge-alarms lwaftr any +$ sudo ./snabb config purge-alarms --by-severity above:minor lwaftr any +$ sudo ./snabb config purge-alarms --by-older-than minutes:5 lwaftr cleared + +See https://github.com/Igalia/snabb/blob/lwaftr/src/program/config/README.md +for full documentation. diff --git a/src/program/config/purge_alarms/README.inc b/src/program/config/purge_alarms/README.inc new file mode 120000 index 0000000000..100b93820a --- /dev/null +++ b/src/program/config/purge_alarms/README.inc @@ -0,0 +1 @@ +README \ No newline at end of file diff --git a/src/program/config/purge_alarms/purge_alarms.lua b/src/program/config/purge_alarms/purge_alarms.lua new file mode 100644 index 0000000000..a1be4d380e --- /dev/null +++ b/src/program/config/purge_alarms/purge_alarms.lua @@ -0,0 +1,56 @@ +-- Use of this source code is governed by the Apache 2.0 license; see COPYING. +module(..., package.seeall) + +local common = require("program.config.common") +local lib = require("core.lib") + +local function usage(exit_code) + print(require('program.config.purge_alarms.README_inc')) + main.exit(exit_code) +end + +local function parse_args (args) + local handlers = {} + local opts = {} + local function table_size (t) + local count = 0 + for _ in pairs(t) do count = count + 1 end + return count + end + local function without_opts (args) + local ret = {} + for i=1,#args do + local arg = args[i] + if opts[arg] then + i = i + 2 + else + table.insert(ret, arg) + end + end + return ret + end + handlers['by-older-than'] = function (arg) opts.older_than = arg end + handlers['by-severity'] = function (arg) opts.severity = arg end + handlers['by-operator-state'] = function (arg) + opts.operator_state_filter = arg + end + args = lib.dogetopt(args, handlers, "", { ['by-older-than']=1, + ['by-severity']=1, ['by-operator-state']=1 }) + opts.status = table.remove(args, #args) + if table_size(opts) == 0 then usage(1) end + local args = without_opts(args) + return opts, args +end + +function run(args) + local l_args, args = parse_args(args) + local opts = { command='purge-alarms', with_path=false, is_config=false } + args = common.parse_command_line(args, opts) + local response = common.call_leader( + args.instance_id, 'purge-alarms', + { schema = args.schema_name, alarm_status = l_args.status, + older_than = l_args.older_than, severity = l_args.severity, + operator_state_filter = l_args.operator_state_filter, + print_default = args.print_default, format = args.format }) + common.print_and_exit(response, "purged_alarms") +end