Merge pull request #883 from kernelkit/yanger

Yanger Modularization
This commit is contained in:
Tobias Waldekranz
2025-01-10 11:21:35 +01:00
committed by GitHub
66 changed files with 7522 additions and 1336 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,374 @@
module ietf-keystore {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:ietf-keystore";
prefix ks;
import ietf-netconf-acm {
prefix nacm;
reference
"RFC 8341: Network Configuration Access Control Model";
}
import ietf-crypto-types {
prefix ct;
reference
"RFC AAAA: YANG Data Types and Groupings for Cryptography";
}
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: https://datatracker.ietf.org/wg/netconf
WG List: NETCONF WG list <mailto:netconf@ietf.org>
Author: Kent Watsen <mailto:kent+ietf@watsen.net>";
description
"This module defines a 'keystore' to centralize management
of security credentials.
Copyright (c) 2023 IETF Trust and the persons identified
as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, is permitted pursuant to, and
subject to the license terms contained in, the Revised
BSD License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC CCCC
(https://www.rfc-editor.org/info/rfcCCCC); see the RFC
itself for full legal notices.
The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
are to be interpreted as described in BCP 14 (RFC 2119)
(RFC 8174) when, and only when, they appear in all
capitals, as shown here.";
revision 2023-12-28 {
description
"Initial version";
reference
"RFC CCCC: A YANG Data Model for a Keystore";
}
feature central-keystore-supported {
description
"The 'central-keystore-supported' feature indicates that
the server supports the central keystore (i.e., fully
implements the 'ietf-keystore' module).";
}
feature inline-definitions-supported {
description
"The 'inline-definitions-supported' feature indicates that
the server supports locally-defined keys.";
}
feature asymmetric-keys {
description
"The 'asymmetric-keys' feature indicates that the server
implements the /keystore/asymmetric-keys subtree.";
}
feature symmetric-keys {
description
"The 'symmetric-keys' feature indicates that the server
implements the /keystore/symmetric-keys subtree.";
}
typedef symmetric-key-ref {
type leafref {
path "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:name";
}
description
"This typedef enables modules to easily define a reference
to a symmetric key stored in the central keystore.";
}
typedef asymmetric-key-ref {
type leafref {
path "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key/ks:name";
}
description
"This typedef enables modules to easily define a reference
to an asymmetric key stored in the central keystore.";
}
grouping encrypted-by-grouping {
description
"A grouping that defines a 'choice' statement that can be
augmented into the 'encrypted-by' node, present in the
'symmetric-key-grouping' and 'asymmetric-key-pair-grouping'
groupings defined in RFC AAAA, enabling references to keys
in the central keystore.";
choice encrypted-by {
nacm:default-deny-write;
mandatory true;
description
"A choice amongst other symmetric or asymmetric keys.";
case symmetric-key-ref {
if-feature "central-keystore-supported";
if-feature "symmetric-keys";
leaf symmetric-key-ref {
type ks:symmetric-key-ref;
description
"Identifies the symmetric key used to encrypt the
associated key.";
}
}
case asymmetric-key-ref {
if-feature "central-keystore-supported";
if-feature "asymmetric-keys";
leaf asymmetric-key-ref {
type ks:asymmetric-key-ref;
description
"Identifies the asymmetric key whose public key
encrypted the associated key.";
}
}
}
}
grouping asymmetric-key-certificate-ref-grouping {
description
"Grouping for the reference to a certificate associated
with an asymmetric key stored in the central keystore.";
leaf asymmetric-key {
nacm:default-deny-write;
if-feature "central-keystore-supported";
if-feature "asymmetric-keys";
type ks:asymmetric-key-ref;
must '../certificate';
description
"A reference to an asymmetric key in the keystore.";
}
leaf certificate {
nacm:default-deny-write;
type leafref {
path "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key[ks:name = current()/../asymmetric-key]/ks:certificates/ks:certificate/ks:name";
}
must '../asymmetric-key';
description
"A reference to a specific certificate of the
asymmetric key in the keystore.";
}
}
grouping inline-or-keystore-symmetric-key-grouping {
description
"A grouping for the configuration of a symmetric key. The
symmetric key may be defined inline or as a reference to
a symmetric key stored in the central keystore.
Servers that do not define the 'central-keystore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate keystore locations.";
choice inline-or-keystore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the keystore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"Container to hold the local key definition.";
uses ct:symmetric-key-grouping;
}
}
case central-keystore {
if-feature "central-keystore-supported";
if-feature "symmetric-keys";
leaf central-keystore-reference {
type ks:symmetric-key-ref;
description
"A reference to an symmetric key that exists in
the central keystore.";
}
}
}
}
grouping inline-or-keystore-asymmetric-key-grouping {
description
"A grouping for the configuration of an asymmetric key. The
asymmetric key may be defined inline or as a reference to
an asymmetric key stored in the central keystore.
Servers that do not define the 'central-keystore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate keystore locations.";
choice inline-or-keystore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the keystore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"Container to hold the local key definition.";
uses ct:asymmetric-key-pair-grouping;
}
}
case central-keystore {
if-feature "central-keystore-supported";
if-feature "asymmetric-keys";
leaf central-keystore-reference {
type ks:asymmetric-key-ref;
description
"A reference to an asymmetric key that exists in
the central keystore. The intent is to reference
just the asymmetric key without any regard for
any certificates that may be associated with it.";
}
}
}
}
grouping inline-or-keystore-asymmetric-key-with-certs-grouping {
description
"A grouping for the configuration of an asymmetric key and
its associated certificates. The asymmetric key and its
associated certificates may be defined inline or as a
reference to an asymmetric key (and its associated
certificates) in the central keystore.
Servers that do not define the 'central-keystore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate keystore locations.";
choice inline-or-keystore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the keystore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"Container to hold the local key definition.";
uses ct:asymmetric-key-pair-with-certs-grouping;
}
}
case central-keystore {
if-feature "central-keystore-supported";
if-feature "asymmetric-keys";
leaf central-keystore-reference {
type ks:asymmetric-key-ref;
description
"A reference to an asymmetric-key (and all of its
associated certificates) in the keystore, when
this module is implemented.";
}
}
}
}
grouping inline-or-keystore-end-entity-cert-with-key-grouping {
description
"A grouping for the configuration of an asymmetric key and
its associated end-entity certificate. The asymmetric key
and its associated end-entity certificate may be defined
inline or as a reference to an asymmetric key (and its
associated end-entity certificate) in the central keystore.
Servers that do not define the 'central-keystore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate keystore locations.";
choice inline-or-keystore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the keystore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"Container to hold the local key definition.";
uses ct:asymmetric-key-pair-with-cert-grouping;
}
}
case central-keystore {
if-feature "central-keystore-supported";
if-feature "asymmetric-keys";
container central-keystore-reference {
description
"A reference to a specific certificate associated with
an asymmetric key stored in the central keystore.";
uses asymmetric-key-certificate-ref-grouping;
}
}
}
}
grouping keystore-grouping {
description
"Grouping definition enables use in other contexts. If ever
done, implementations MUST augment new 'case' statements
into the various inline-or-keystore 'choice' statements to
supply leafrefs to the model-specific location(s).";
container asymmetric-keys {
nacm:default-deny-write;
if-feature "asymmetric-keys";
description
"A list of asymmetric keys.";
list asymmetric-key {
key "name";
description
"An asymmetric key.";
leaf name {
type string;
description
"An arbitrary name for the asymmetric key.";
}
uses ct:asymmetric-key-pair-with-certs-grouping;
}
}
container symmetric-keys {
nacm:default-deny-write;
if-feature "symmetric-keys";
description
"A list of symmetric keys.";
list symmetric-key {
key "name";
description
"A symmetric key.";
leaf name {
type string;
description
"An arbitrary name for the symmetric key.";
}
uses ct:symmetric-key-grouping;
}
}
}
container keystore {
nacm:default-deny-write;
if-feature "central-keystore-supported";
description
"A central keystore containing a list of symmetric keys and
a list of asymmetric keys.";
uses keystore-grouping {
augment "symmetric-keys/symmetric-key/key-type/encrypted-key/encrypted-key/encrypted-by" {
description
"Augments in a choice statement enabling the encrypting
key to be any other symmetric or asymmetric key in the
central keystore.";
uses encrypted-by-grouping;
}
augment "asymmetric-keys/asymmetric-key/private-key-type/encrypted-private-key/encrypted-private-key/encrypted-by" {
description
"Augments in a choice statement enabling the encrypting
key to be any other symmetric or asymmetric key in the
central keystore.";
uses encrypted-by-grouping;
}
}
}
}
@@ -0,0 +1,423 @@
module ietf-netconf-acm {
namespace "urn:ietf:params:xml:ns:yang:ietf-netconf-acm";
prefix nacm;
import ietf-yang-types {
prefix yang;
}
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: <https://datatracker.ietf.org/wg/netconf/>
WG List: <mailto:netconf@ietf.org>
Author: Andy Bierman
<mailto:andy@yumaworks.com>
Author: Martin Bjorklund
<mailto:mbj@tail-f.com>";
description
"Network Configuration Access Control Model.
Copyright (c) 2012 - 2018 IETF Trust and the persons
identified as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with or
without modification, is permitted pursuant to, and subject
to the license terms contained in, the Simplified BSD
License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC 8341; see
the RFC itself for full legal notices.";
revision 2018-02-14 {
description
"Added support for YANG 1.1 actions and notifications tied to
data nodes. Clarified how NACM extensions can be used by
other data models.";
reference
"RFC 8341: Network Configuration Access Control Model";
}
revision 2012-02-22 {
description
"Initial version.";
reference
"RFC 6536: Network Configuration Protocol (NETCONF)
Access Control Model";
}
extension default-deny-write {
description
"Used to indicate that the data model node
represents a sensitive security system parameter.
If present, the NETCONF server will only allow the designated
'recovery session' to have write access to the node. An
explicit access control rule is required for all other users.
If the NACM module is used, then it must be enabled (i.e.,
/nacm/enable-nacm object equals 'true'), or this extension
is ignored.
The 'default-deny-write' extension MAY appear within a data
definition statement. It is ignored otherwise.";
}
extension default-deny-all {
description
"Used to indicate that the data model node
controls a very sensitive security system parameter.
If present, the NETCONF server will only allow the designated
'recovery session' to have read, write, or execute access to
the node. An explicit access control rule is required for all
other users.
If the NACM module is used, then it must be enabled (i.e.,
/nacm/enable-nacm object equals 'true'), or this extension
is ignored.
The 'default-deny-all' extension MAY appear within a data
definition statement, 'rpc' statement, or 'notification'
statement. It is ignored otherwise.";
}
typedef user-name-type {
type string {
length "1..max";
}
description
"General-purpose username string.";
}
typedef matchall-string-type {
type string {
pattern '\*';
}
description
"The string containing a single asterisk '*' is used
to conceptually represent all possible values
for the particular leaf using this data type.";
}
typedef access-operations-type {
type bits {
bit create {
description
"Any protocol operation that creates a
new data node.";
}
bit read {
description
"Any protocol operation or notification that
returns the value of a data node.";
}
bit update {
description
"Any protocol operation that alters an existing
data node.";
}
bit delete {
description
"Any protocol operation that removes a data node.";
}
bit exec {
description
"Execution access to the specified protocol operation.";
}
}
description
"Access operation.";
}
typedef group-name-type {
type string {
length "1..max";
pattern '[^\*].*';
}
description
"Name of administrative group to which
users can be assigned.";
}
typedef action-type {
type enumeration {
enum "permit" {
description
"Requested action is permitted.";
}
enum "deny" {
description
"Requested action is denied.";
}
}
description
"Action taken by the server when a particular
rule matches.";
}
typedef node-instance-identifier {
type yang:xpath1.0;
description
"Path expression used to represent a special
data node, action, or notification instance-identifier
string.
A node-instance-identifier value is an
unrestricted YANG instance-identifier expression.
All the same rules as an instance-identifier apply,
except that predicates for keys are optional. If a key
predicate is missing, then the node-instance-identifier
represents all possible server instances for that key.
This XML Path Language (XPath) expression is evaluated in the
following context:
o The set of namespace declarations are those in scope on
the leaf element where this type is used.
o The set of variable bindings contains one variable,
'USER', which contains the name of the user of the
current session.
o The function library is the core function library, but
note that due to the syntax restrictions of an
instance-identifier, no functions are allowed.
o The context node is the root node in the data tree.
The accessible tree includes actions and notifications tied
to data nodes.";
}
container nacm {
nacm:default-deny-all;
description
"Parameters for NETCONF access control model.";
leaf enable-nacm {
type boolean;
default "true";
description
"Enables or disables all NETCONF access control
enforcement. If 'true', then enforcement
is enabled. If 'false', then enforcement
is disabled.";
}
leaf read-default {
type action-type;
default "permit";
description
"Controls whether read access is granted if
no appropriate rule is found for a
particular read request.";
}
leaf write-default {
type action-type;
default "deny";
description
"Controls whether create, update, or delete access
is granted if no appropriate rule is found for a
particular write request.";
}
leaf exec-default {
type action-type;
default "permit";
description
"Controls whether exec access is granted if no appropriate
rule is found for a particular protocol operation request.";
}
leaf enable-external-groups {
type boolean;
default "true";
description
"Controls whether the server uses the groups reported by the
NETCONF transport layer when it assigns the user to a set of
NACM groups. If this leaf has the value 'false', any group
names reported by the transport layer are ignored by the
server.";
}
leaf denied-operations {
type yang:zero-based-counter32;
config false;
mandatory true;
description
"Number of times since the server last restarted that a
protocol operation request was denied.";
}
leaf denied-data-writes {
type yang:zero-based-counter32;
config false;
mandatory true;
description
"Number of times since the server last restarted that a
protocol operation request to alter
a configuration datastore was denied.";
}
leaf denied-notifications {
type yang:zero-based-counter32;
config false;
mandatory true;
description
"Number of times since the server last restarted that
a notification was dropped for a subscription because
access to the event type was denied.";
}
container groups {
description
"NETCONF access control groups.";
list group {
key "name";
description
"One NACM group entry. This list will only contain
configured entries, not any entries learned from
any transport protocols.";
leaf name {
type group-name-type;
description
"Group name associated with this entry.";
}
leaf-list user-name {
type user-name-type;
description
"Each entry identifies the username of
a member of the group associated with
this entry.";
}
}
}
list rule-list {
key "name";
ordered-by user;
description
"An ordered collection of access control rules.";
leaf name {
type string {
length "1..max";
}
description
"Arbitrary name assigned to the rule-list.";
}
leaf-list group {
type union {
type matchall-string-type;
type group-name-type;
}
description
"List of administrative groups that will be
assigned the associated access rights
defined by the 'rule' list.
The string '*' indicates that all groups apply to the
entry.";
}
list rule {
key "name";
ordered-by user;
description
"One access control rule.
Rules are processed in user-defined order until a match is
found. A rule matches if 'module-name', 'rule-type', and
'access-operations' match the request. If a rule
matches, the 'action' leaf determines whether or not
access is granted.";
leaf name {
type string {
length "1..max";
}
description
"Arbitrary name assigned to the rule.";
}
leaf module-name {
type union {
type matchall-string-type;
type string;
}
default "*";
description
"Name of the module associated with this rule.
This leaf matches if it has the value '*' or if the
object being accessed is defined in the module with the
specified module name.";
}
choice rule-type {
description
"This choice matches if all leafs present in the rule
match the request. If no leafs are present, the
choice matches all requests.";
case protocol-operation {
leaf rpc-name {
type union {
type matchall-string-type;
type string;
}
description
"This leaf matches if it has the value '*' or if
its value equals the requested protocol operation
name.";
}
}
case notification {
leaf notification-name {
type union {
type matchall-string-type;
type string;
}
description
"This leaf matches if it has the value '*' or if its
value equals the requested notification name.";
}
}
case data-node {
leaf path {
type node-instance-identifier;
mandatory true;
description
"Data node instance-identifier associated with the
data node, action, or notification controlled by
this rule.
Configuration data or state data
instance-identifiers start with a top-level
data node. A complete instance-identifier is
required for this type of path value.
The special value '/' refers to all possible
datastore contents.";
}
}
}
leaf access-operations {
type union {
type matchall-string-type;
type access-operations-type;
}
default "*";
description
"Access operations associated with this rule.
This leaf matches if it has the value '*' or if the
bit corresponding to the requested operation is set.";
}
leaf action {
type action-type;
mandatory true;
description
"The access control action associated with the
rule. If a rule has been determined to match a
particular request, then this object is used
to determine whether to permit or deny the
request.";
}
leaf comment {
type string;
description
"A textual description of the access rule.";
}
}
}
}
}
@@ -0,0 +1,359 @@
module ietf-truststore {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:ietf-truststore";
prefix ts;
import ietf-netconf-acm {
prefix nacm;
reference
"RFC 8341: Network Configuration Access Control Model";
}
import ietf-crypto-types {
prefix ct;
reference
"RFC AAAA: YANG Data Types and Groupings for Cryptography";
}
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web : https://datatracker.ietf.org/wg/netconf
WG List : NETCONF WG list <mailto:netconf@ietf.org>
Author : Kent Watsen <kent+ietf@watsen.net>";
description
"This module defines a 'truststore' to centralize management
of trust anchors including certificates and public keys.
Copyright (c) 2023 IETF Trust and the persons identified
as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, is permitted pursuant to, and
subject to the license terms contained in, the Revised
BSD License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC BBBB
(https://www.rfc-editor.org/info/rfcBBBB); see the RFC
itself for full legal notices.
The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
are to be interpreted as described in BCP 14 (RFC 2119)
(RFC 8174) when, and only when, they appear in all
capitals, as shown here.";
revision 2023-12-28 {
description
"Initial version";
reference
"RFC BBBB: A YANG Data Model for a Truststore";
}
feature central-truststore-supported {
description
"The 'central-truststore-supported' feature indicates that
the server supports the truststore (i.e., implements the
'ietf-truststore' module).";
}
feature inline-definitions-supported {
description
"The 'inline-definitions-supported' feature indicates that
the server supports locally-defined trust anchors.";
}
feature certificates {
description
"The 'certificates' feature indicates that the server
implements the /truststore/certificate-bags subtree.";
}
feature public-keys {
description
"The 'public-keys' feature indicates that the server
implements the /truststore/public-key-bags subtree.";
}
typedef certificate-bag-ref {
type leafref {
path "/ts:truststore/ts:certificate-bags/ts:certificate-bag/ts:name";
}
description
"This typedef defines a reference to a certificate bag
in the central truststore.";
}
typedef certificate-ref {
type leafref {
path "/ts:truststore/ts:certificate-bags/ts:certificate-bag[ts:name = current()/../certificate-bag]/ts:certificate/ts:name";
}
description
"This typedef defines a reference to a specific certificate
in a certificate bag in the central truststore. This typedef
requires that there exist a sibling 'leaf' node called
'certificate-bag' that SHOULD have the typedef
'certificate-bag-ref'.";
}
typedef public-key-bag-ref {
type leafref {
path "/ts:truststore/ts:public-key-bags/ts:public-key-bag/ts:name";
}
description
"This typedef defines a reference to a public key bag
in the central truststore.";
}
typedef public-key-ref {
type leafref {
path "/ts:truststore/ts:public-key-bags/ts:public-key-bag[ts:name = current()/../public-key-bag]/ts:public-key/ts:name";
}
description
"This typedef defines a reference to a specific public key
in a public key bag in the truststore. This typedef
requires that there exist a sibling 'leaf' node called
'public-key-bag' that SHOULD have the typedef
'public-key-bag-ref'.";
}
grouping certificate-ref-grouping {
description
"Grouping for the reference to a certificate in a
certificate-bag in the central truststore.";
leaf certificate-bag {
nacm:default-deny-write;
if-feature "central-truststore-supported";
if-feature "certificates";
type ts:certificate-bag-ref;
must "../certificate";
description
"Reference to a certificate-bag in the truststore.";
}
leaf certificate {
nacm:default-deny-write;
type ts:certificate-ref;
must "../certificate-bag";
description
"Reference to a specific certificate in the
referenced certificate-bag.";
}
}
grouping public-key-ref-grouping {
description
"Grouping for the reference to a public key in a
public-key-bag in the central truststore.";
leaf public-key-bag {
nacm:default-deny-write;
if-feature "central-truststore-supported";
if-feature "public-keys";
type ts:public-key-bag-ref;
description
"Reference of a public key bag in the truststore inlucding
the certificate to authenticate the TLS client.";
}
leaf public-key {
nacm:default-deny-write;
type ts:public-key-ref;
description
"Reference to a specific public key in the
referenced public-key-bag.";
}
}
grouping inline-or-truststore-certs-grouping {
description
"A grouping for the configuration of a list of certificates.
The list of certificate may be defined inline or as a
reference to a certificate bag in the central truststore.
Servers that do not define the 'central-truststore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate truststore locations.";
choice inline-or-truststore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the truststore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"A container for locally configured trust anchor
certificates.";
list certificate {
key "name";
min-elements 1;
description
"A trust anchor certificate.";
leaf name {
type string;
description
"An arbitrary name for this certificate.";
}
uses ct:trust-anchor-cert-grouping {
refine "cert-data" {
mandatory true;
}
}
}
}
}
case central-truststore {
if-feature "central-truststore-supported";
if-feature "certificates";
leaf central-truststore-reference {
type ts:certificate-bag-ref;
description
"A reference to a certificate bag that exists in the
central truststore.";
}
}
}
}
grouping inline-or-truststore-public-keys-grouping {
description
"A grouping that allows the public keys to be either
configured locally, within the using data model, or be a
reference to a public key bag stored in the truststore.
Servers that do not define the 'central-truststore-supported'
feature SHOULD augment in custom 'case' statements enabling
references to alternate truststore locations.";
choice inline-or-truststore {
nacm:default-deny-write;
mandatory true;
description
"A choice between an inlined definition and a definition
that exists in the truststore.";
case inline {
if-feature "inline-definitions-supported";
container inline-definition {
description
"A container to hold local public key definitions.";
list public-key {
key "name";
description
"A public key definition.";
leaf name {
type string;
description
"An arbitrary name for this public key.";
}
uses ct:public-key-grouping;
}
}
}
case central-truststore {
if-feature "central-truststore-supported";
if-feature "public-keys";
leaf central-truststore-reference {
type ts:public-key-bag-ref;
description
"A reference to a bag of public keys that exists
in the central truststore.";
}
}
}
}
grouping truststore-grouping {
description
"A grouping definition that enables use in other contexts.
Where used, implementations MUST augment new 'case'
statements into the various inline-or-truststore 'choice'
statements to supply leafrefs to the model-specific
location(s).";
container certificate-bags {
nacm:default-deny-write;
if-feature "certificates";
description
"A collection of certificate bags.";
list certificate-bag {
key "name";
description
"A bag of certificates. Each bag of certificates SHOULD
be for a specific purpose. For instance, one bag could
be used to authenticate a specific set of servers, while
another could be used to authenticate a specific set of
clients.";
leaf name {
type string;
description
"An arbitrary name for this bag of certificates.";
}
leaf description {
type string;
description
"A description for this bag of certificates. The
intended purpose for the bag SHOULD be described.";
}
list certificate {
key "name";
description
"A trust anchor certificate.";
leaf name {
type string;
description
"An arbitrary name for this certificate.";
}
uses ct:trust-anchor-cert-grouping {
refine "cert-data" {
mandatory true;
}
}
}
}
}
container public-key-bags {
nacm:default-deny-write;
if-feature "public-keys";
description
"A collection of public key bags.";
list public-key-bag {
key "name";
description
"A bag of public keys. Each bag of keys SHOULD be for
a specific purpose. For instance, one bag could be used
authenticate a specific set of servers, while another
could be used to authenticate a specific set of clients.";
leaf name {
type string;
description
"An arbitrary name for this bag of public keys.";
}
leaf description {
type string;
description
"A description for this bag public keys. The
intended purpose for the bag SHOULD be described.";
}
list public-key {
key "name";
description
"A public key.";
leaf name {
type string;
description
"An arbitrary name for this public key.";
}
uses ct:public-key-grouping;
}
}
}
}
container truststore {
nacm:default-deny-write;
if-feature "central-truststore-supported";
description
"The truststore contains bags of certificates and
public keys.";
uses truststore-grouping;
}
}
+1 -4
View File
@@ -71,9 +71,6 @@ module infix-ethernet-interface {
}
/* Deviations for statistics */
deviation "/if:interfaces/if:interface/eth:ethernet/eth:statistics/eth:frame/eth:in-total-frames" {
deviate not-supported;
}
deviation "/if:interfaces/if:interface/eth:ethernet/eth:statistics/eth:frame/eth:out-error-mac-internal-frames" {
deviate not-supported;
}
@@ -82,5 +79,5 @@ module infix-ethernet-interface {
}
deviation "/if:interfaces/if:interface/eth:ethernet/eth:statistics/eth:mac-control" {
deviate not-supported;
}
}
}
+1
View File
@@ -0,0 +1 @@
*.pyc
+5 -5
View File
@@ -4,9 +4,9 @@ version = "1.0"
description = "Linux to infix-YANG"
license = "MIT"
packages = [
{ include = "yanger/*.py" },
{ include = "cli_pretty/*.py" },
{ include = "ospf_status/*.py" }
{ include = "yanger" },
{ include = "cli_pretty" },
{ include = "ospf_status" }
]
authors = [
"KernelKit developers"
@@ -18,6 +18,6 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
yanger = "yanger:main"
yanger = "yanger.__main__:main"
cli-pretty = "cli_pretty:main"
ospf-status = "ospf_status:main"
ospf-status = "ospf_status:main"
-4
View File
@@ -1,4 +0,0 @@
from .yanger import main
if __name__ == "__main__":
main()
+63
View File
@@ -0,0 +1,63 @@
import logging
import logging.handlers
import subprocess
import json
import sys # (built-in module)
import os
import argparse
from . import common
from . import host
def main():
parser = argparse.ArgumentParser(description="YANG data creator")
parser.add_argument("model", help="YANG Model")
parser.add_argument("-p", "--param", default=None, help="Model dependent parameter")
parser.add_argument("-t", "--test", default=None, help="Test data base path")
args = parser.parse_args()
# Set up syslog output for critical errors to aid debugging
common.LOG = logging.getLogger('yanger')
if os.path.exists('/dev/log'):
log = logging.handlers.SysLogHandler(address='/dev/log')
else:
# Use /dev/null as a fallback for unit tests
log = logging.FileHandler('/dev/null')
fmt = logging.Formatter('%(name)s[%(process)d]: %(message)s')
log.setFormatter(fmt)
common.LOG.setLevel(logging.INFO)
common.LOG.addHandler(log)
if args.test:
host.HOST = host.Testhost(args.test)
else:
host.HOST = host.Localhost()
if args.model == 'ietf-interfaces':
from . import ietf_interfaces
yang_data = ietf_interfaces.operational(args.param)
elif args.model == 'ietf-routing':
from . import ietf_routing
yang_data = ietf_routing.operational()
elif args.model == 'ietf-ospf':
from . import ietf_ospf
yang_data = ietf_ospf.operational()
elif args.model == 'ietf-hardware':
from . import ietf_hardware
yang_data = ietf_hardware.operational()
elif args.model == 'infix-containers':
from . import infix_containers
yang_data = infix_containers.operational()
elif args.model == 'ietf-system':
from . import ietf_system
yang_data = ietf_system.operational()
else:
common.LOG.warning(f"Unsupported model {args.model}", file=sys.stderr)
sys.exit(1)
print(json.dumps(yang_data, indent=2))
if __name__ == "__main__":
main()
+27
View File
@@ -0,0 +1,27 @@
LOG = None
def lookup(obj, *keys):
"""This function returns a value from a nested json object"""
curr = obj
for key in keys:
if isinstance(curr, dict) and key in curr:
curr = curr[key]
else:
return None
return curr
def insert(obj, *path_and_value):
""""This function inserts a value into a nested json object"""
if len(path_and_value) < 2:
raise ValueError("Error: insert() takes at least two args")
*path, value = path_and_value
curr = obj
for key in path[:-1]:
if key not in curr or not isinstance(curr[key], dict):
curr[key] = {}
curr = curr[key]
curr[path[-1]] = value
+133
View File
@@ -0,0 +1,133 @@
import abc
import datetime
import json
import os
import subprocess
from . import common
HOST = None
class Host(abc.ABC):
"""Host system API"""
@abc.abstractmethod
def now(self):
"""Get the current time as a `datetime`"""
pass
@abc.abstractmethod
def run(self, cmd, default=None, log=True):
"""Get stdout of cmd
Run cmd, provided as an argv, and return the output it
produces. On error, return the default value if provided,
otherwise raise an exception.
"""
pass
def run_multiline(self, cmd, default=None):
"""Get lines of stdout of cmd"""
try:
txt = self.run(cmd, log=(default is None))
return txt.splitlines()
except:
if default is not None:
return default
raise
def run_json(self, cmd, default=None):
"""Get JSON object from stdout of cmd"""
try:
txt = self.run(cmd, log=(default is None))
return json.loads(txt)
except:
if default is not None:
return default
raise
@abc.abstractmethod
def read(self, path):
"""Get the contents of path
Returns `None` if the file is not readable for any reason.
"""
pass
def read_json(self, path, default=None):
"""Get JSON object from path """
try:
txt = self.read(path)
return json.loads(txt)
except:
if default is not None:
return default
raise
class Localhost(Host):
def now(self):
return datetime.datetime.now(tz=datetime.timezone.utc)
def run(self, cmd, default=None, log=True):
try:
result = subprocess.run(cmd, check=True, text=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
return result.stdout
except subprocess.CalledProcessError as err:
if default is not None:
return default
if log:
common.LOG.error(f"Failed to run {err}")
raise
def read(self, path):
try:
with open(path, 'r') as f:
data = f.read().strip()
return data
except FileNotFoundError:
# This is considered OK
pass
except IOError:
common.LOG.error(f"Failed to read \"{path}\"")
return None
class Testhost(Host):
def __init__(self, basedir):
self.basedir = basedir
def now(self):
return datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
def run(self, cmd, default=None, log=True):
slug = "_".join(cmd).replace("/", "+").replace(" ", "-")
path = os.path.join(self.basedir, "run", slug)
try:
with open(path, 'r') as f:
return f.read()
except:
if default is not None:
return default
if log:
common.LOG.error(f"No recording found for run \"{path}\"")
raise
def read(self, path):
path = os.path.join(self.basedir, "rootfs", path[1:])
try:
with open(path, 'r') as f:
return f.read()
except FileNotFoundError:
# This is considered OK
pass
except:
common.LOG.error(f"No recording found for file \"{path}\"")
raise
+90
View File
@@ -0,0 +1,90 @@
import datetime
import os
from .common import insert
from .host import HOST
def vpd_vendor_extensions(data):
vendor_extensions = []
for ext in data:
vendor_extension = {}
vendor_extension["iana-enterprise-number"] = ext[0]
vendor_extension["extension-data"] = ext[1]
vendor_extensions.append(vendor_extension)
return vendor_extensions
def vpd_component(vpd):
component = {}
component["name"] = vpd.get("board")
component["infix-hardware:vpd-data"] = {}
if vpd.get("data"):
component["class"] = "infix-hardware:vpd"
if vpd["data"].get("manufacture-date"):
mfgdate = datetime.datetime.strptime(vpd["data"]["manufacture-date"],
"%m/%d/%Y %H:%M:%S")
component["mfg-date"] = mfgdate.strftime("%Y-%m-%dT%H:%M:%SZ")
if vpd["data"].get("manufacter"):
component["mfg-name"] = vpd["data"]["manufacturer"]
if vpd["data"].get("product-name"):
component["model-name"] = vpd["data"]["product-name"]
if vpd["data"].get("serial-number"):
component["serial-num"] = vpd["data"]["serial-number"]
# Set VPD-data entrys
for k, v in vpd["data"].items():
if vpd["data"].get(k):
if k != "vendor-extension":
component["infix-hardware:vpd-data"][k] = v
else:
vendor_extensions=vpd_vendor_extensions(v)
component["infix-hardware:vpd-data"]["infix-hardware:vendor-extension"] = vendor_extensions
return component
def vpd_components(systemjson):
return [vpd_component(vpd) for vpd in systemjson.get("vpd", {}).values()]
def usb_port_components(systemjson):
usb_ports = systemjson.get("usb-ports", [])
ports=[]
names=[]
for usb_port in usb_ports:
port={}
if usb_port.get("path"):
if usb_port["name"] in names:
continue
path = usb_port["path"]
if os.path.basename(path) == "authorized_default":
# TODO: Untestable. Should be done via the host API
if os.path.exists(path):
with open(path, "r") as f:
names.append(usb_port["name"])
data = int(f.readline().strip())
enabled = "unlocked" if data == 1 else "locked"
port["state"] = {}
port["state"]["admin-state"] = enabled
port["name"] = usb_port["name"]
port["class"] = "infix-hardware:usb"
port["state"]["oper-state"] = "enabled"
ports.append(port)
return ports
def operational():
systemjson = HOST.read_json("/run/system.json")
return {
"ietf-hardware:hardware": {
"component":
vpd_components(systemjson) +
usb_port_components(systemjson) +
[],
},
}
@@ -0,0 +1,564 @@
from ..common import insert, lookup, LOG
from ..host import HOST
def json_get_yang_type(iface_in):
if iface_in['link_type'] == "loopback":
return "infix-if-type:loopback"
if iface_in['link_type'] in ("gre", "gre6"):
return "infix-if-type:gre"
if iface_in['link_type'] != "ether":
return "infix-if-type:other"
if 'parentbus' in iface_in and iface_in['parentbus'] == "virtio":
return "infix-if-type:etherlike"
if 'linkinfo' not in iface_in:
return "infix-if-type:ethernet"
if 'info_kind' not in iface_in['linkinfo']:
return "infix-if-type:ethernet"
if iface_in['linkinfo']['info_kind'] == "veth":
return "infix-if-type:veth"
if iface_in['linkinfo']['info_kind'] in ("gretap", "ip6gretap"):
return "infix-if-type:gretap"
if iface_in['linkinfo']['info_kind'] == "vlan":
return "infix-if-type:vlan"
if iface_in['linkinfo']['info_kind'] == "bridge":
return "infix-if-type:bridge"
if iface_in['linkinfo']['info_kind'] == "dsa":
return "infix-if-type:ethernet"
if iface_in['linkinfo']['info_kind'] == "dummy":
return "infix-if-type:dummy"
# Fallback
return "infix-if-type:ethernet"
def json_get_yang_origin(addr):
"""Translate kernel IP address origin to YANG"""
xlate = {
"kernel_ll": "link-layer",
"kernel_ra": "link-layer",
"static": "static",
"dhcp": "dhcp",
"random": "random",
}
proto = addr['protocol']
if proto in ("kernel_ll", "kernel_ra"):
if "stable-privacy" in addr:
return "random"
return xlate.get(proto, "other")
def iface_is_dsa(iface_in):
"""Check if interface is a DSA/intra-switch port"""
if "linkinfo" not in iface_in:
return False
if "info_kind" not in iface_in['linkinfo']:
return False
if iface_in['linkinfo']['info_kind'] != "dsa":
return False
return True
def get_bridge_port_pvid(ifname):
data = HOST.run_json(['bridge', '-j', 'vlan', 'show', 'dev', ifname])
if len(data) != 1:
return None
iface = data[0]
for vlan in iface['vlans']:
if 'flags' in vlan and 'PVID' in vlan['flags']:
return vlan['vlan']
return None
def get_bridge_port_stp_state(ifname):
data = HOST.run_json(['bridge', '-j', 'link', 'show', 'dev', ifname])
if len(data) != 1:
return None
iface = data[0]
states = ['disabled', 'listening', 'learning', 'forwarding', 'blocking']
if 'state' in iface and iface['state'] in states:
return iface['state']
return None
def get_brport_multicast(ifname):
"""Check if multicast snooping is enabled on bridge, default: nope"""
data = HOST.run_json(['mctl', '-p', 'show', 'igmp', 'json'], default={})
multicast = {}
if ifname in data.get('fast-leave-ports', []):
multicast["fast-leave"] = True
else:
multicast["fast-leave"] = False
if ifname in data.get('multicast-router-ports', []):
multicast["router"] = "permanent"
else:
multicast["router"] = "auto"
return multicast
# We always get all interfaces for two reasons.
# 1) To increase speed on large iron with many ports.
# 2) To simplify testing (single dummy file ip-link-show.json).
def get_ip_link():
"""Fetch interface link information from kernel"""
return HOST.run_json(['ip', '-s', '-d', '-j', 'link', 'show'])
def netns_get_ip_link(netns):
"""Fetch interface link information from within a network namespace"""
return HOST.run_json(['ip', 'netns', 'exec', netns, 'ip', '-s', '-d', '-j', 'link', 'show'])
def get_ip_addr():
"""Fetch interface address information from kernel"""
return HOST.run_json(['ip', '-j', 'addr', 'show'])
def netns_get_ip_addr(netns):
"""Fetch interface address information from within a network namespace"""
return HOST.run_json(['ip', 'netns', 'exec', netns, 'ip', '-j', 'addr', 'show'])
def get_netns_list():
"""Fetch a list of network namespaces"""
return HOST.run_json(['ip', '-j', 'netns', 'list'], [])
def netns_find_ifname(ifname):
"""Find which network namespace owns ifname (if any)"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if 'ifalias' in iface and iface['ifalias'] == ifname:
return netns['name']
return None
def netns_ifindex_to_ifname(ifindex):
"""Look through all network namespaces for an interface index and return its name"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if iface['ifindex'] == ifindex:
if 'ifalias' in iface:
return iface['ifalias']
if 'ifname' in iface:
return iface['ifname']
return None
return None
def add_bridge_port_common(ifname, iface_in, iface_out):
li = iface_in.get("linkinfo", {})
if not (li.get("info_slave_kind") == "bridge" or \
li.get("info_kind") == "bridge"):
return
pvid = get_bridge_port_pvid(ifname)
if pvid is not None:
insert(iface_out, "infix-interfaces:bridge-port", "pvid", pvid)
def add_bridge_port_lower(ifname, iface_in, iface_out):
li = iface_in.get("linkinfo", {})
if not li.get("info_slave_kind") == "bridge":
return
insert(iface_out, "infix-interfaces:bridge-port", "bridge", iface_in['master'])
stp_state = get_bridge_port_stp_state(ifname)
if stp_state is not None:
insert(iface_out, "infix-interfaces:bridge-port", "stp-state", stp_state)
multicast = get_brport_multicast(ifname)
insert(iface_out, "infix-interfaces:bridge-port", "multicast", multicast)
def add_gre(iface_in, iface_out):
if 'link_type' in iface_in:
val = json_get_yang_type(iface_in)
if val != "infix-if-type:gre" and val != "infix-if-type:gretap":
return;
gre={}
info_data=iface_in.get("linkinfo", {}).get("info_data", {})
gre["local"] = info_data.get("local")
gre["remote"] = info_data.get("remote")
insert(iface_out, "infix-interfaces:gre", gre)
def add_ip_link(ifname, iface_in, iface_out):
if 'ifname' in iface_in:
iface_out['name'] = ifname
if 'ifindex' in iface_in:
iface_out['if-index'] = iface_in['ifindex']
if 'ifalias' in iface_in:
iface_out['description'] = iface_in['ifalias']
if 'address' in iface_in and not "POINTOPOINT" in iface_in["flags"]:
iface_out['phys-address'] = iface_in['address']
add_bridge_port_common(ifname, iface_in, iface_out)
add_bridge_port_lower(ifname, iface_in, iface_out)
if not iface_is_dsa(iface_in):
if iface_in.get('link'):
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", iface_in['link'])
elif 'link_index' in iface_in:
# 'link_index' is the only reference we have if the link iface is in a namespace
lower = netns_ifindex_to_ifname(iface_in['link_index'])
if lower:
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", lower)
if 'flags' in iface_in:
iface_out['admin-status'] = "up" if "UP" in iface_in['flags'] else "down"
if 'operstate' in iface_in:
xlate = {
"DOWN": "down",
"UP": "up",
"DORMANT": "dormant",
"TESTING": "testing",
"LOWERLAYERDOWN": "lower-layer-down",
"NOTPRESENT": "not-present"
}
val = xlate.get(iface_in['operstate'], "unknown")
iface_out['oper-status'] = val
if 'link_type' in iface_in:
val = json_get_yang_type(iface_in)
iface_out['type'] = val
add_gre(iface_in, iface_out)
val = lookup(iface_in, "stats64", "rx", "bytes")
if val is not None:
insert(iface_out, "statistics", "out-octets", str(val))
val = lookup(iface_in, "stats64", "tx", "bytes")
if val is not None:
insert(iface_out, "statistics", "in-octets", str(val))
def add_ip_addr(ifname, iface_in, iface_out):
if 'mtu' in iface_in and ifname != "lo":
insert(iface_out, "ietf-ip:ipv4", "mtu", iface_in['mtu'])
val = HOST.read(f"/proc/sys/net/ipv6/conf/{ifname}/mtu")
if val is not None:
insert(iface_out, "ietf-ip:ipv6", "mtu", int(val.strip()))
if 'addr_info' in iface_in:
inet = []
inet6 = []
for addr in iface_in['addr_info']:
new = {}
if 'family' not in addr:
LOG.error("'family' missing from 'addr_info'")
continue
if 'local' in addr:
new['ip'] = addr['local']
if 'prefixlen' in addr:
new['prefix-length'] = addr['prefixlen']
if 'protocol' in addr:
new['origin'] = json_get_yang_origin(addr)
if addr['family'] == "inet":
inet.append(new)
elif addr['family'] == "inet6":
inet6.append(new)
else:
LOG.error("invalid 'family' in 'addr_info'")
sys.exit(1)
insert(iface_out, "ietf-ip:ipv4", "address", inet)
insert(iface_out, "ietf-ip:ipv6", "address", inet6)
def add_ethtool_groups(ifname, iface_out):
"""Fetch interface counters from kernel (need new JSON format!)"""
cmd = ['ethtool', '--json', '-S', ifname, '--all-groups']
try:
data = HOST.run_json(cmd)
if len(data) != 1:
LOG.warning("%s: no counters available, skipping.", ifname)
return
except subprocess.CalledProcessError:
# Allow comand to fail, not all NICs support --json yet
return
iface_in = data[0]
# TODO: room for improvement, the "frame" creation could be more dynamic.
if "eth-mac" in iface_in or "rmon" in iface_in:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "statistics", "frame", {})
frame = iface_out['ieee802-ethernet-interface:ethernet']['statistics']['frame']
if "eth-mac" in iface_in:
mac_in = iface_in['eth-mac']
if "FramesTransmittedOK" in mac_in:
frame['out-frames'] = str(mac_in['FramesTransmittedOK'])
if "MulticastFramesXmittedOK" in mac_in:
frame['out-multicast-frames'] = str(mac_in['MulticastFramesXmittedOK'])
if "BroadcastFramesXmittedOK" in mac_in:
frame['out-broadcast-frames'] = str(mac_in['BroadcastFramesXmittedOK'])
if "FramesReceivedOK" in mac_in:
frame['in-frames'] = str(mac_in['FramesReceivedOK'])
if "MulticastFramesReceivedOK" in mac_in:
frame['in-multicast-frames'] = str(mac_in['MulticastFramesReceivedOK'])
if "BroadcastFramesReceivedOK" in mac_in:
frame['in-broadcast-frames'] = str(mac_in['BroadcastFramesReceivedOK'])
if "FrameCheckSequenceErrors" in mac_in:
frame['in-error-fcs-frames'] = str(mac_in['FrameCheckSequenceErrors'])
if "FramesLostDueToIntMACRcvError" in mac_in:
frame['in-error-mac-internal-frames'] = str(mac_in['FramesLostDueToIntMACRcvError'])
if "OctetsTransmittedOK" in mac_in:
frame['infix-ethernet-interface:out-good-octets'] = str(mac_in['OctetsTransmittedOK'])
if "OctetsReceivedOK" in mac_in:
frame['infix-ethernet-interface:in-good-octets'] = str(mac_in['OctetsReceivedOK'])
tot = 0
found = False
if "FramesReceivedOK" in mac_in:
tot += mac_in['FramesReceivedOK']
found = True
if "FrameCheckSequenceErrors" in mac_in:
tot += mac_in['FrameCheckSequenceErrors']
found = True
if "FramesLostDueToIntMACRcvError" in mac_in:
tot += mac_in['FramesLostDueToIntMACRcvError']
found = True
if "AlignmentErrors" in mac_in:
tot += mac_in['AlignmentErrors']
found = True
if "etherStatsOversizePkts" in mac_in:
tot += mac_in['etherStatsOversizePkts']
found = True
if "etherStatsJabbers" in mac_in:
tot += mac_in['etherStatsJabbers']
found = True
if found:
frame['in-total-frames'] = str(tot)
if "rmon" in iface_in:
rmon_in = iface_in['rmon']
if "undersize_pkts" in rmon_in:
frame['in-error-undersize-frames'] = str(rmon_in['undersize_pkts'])
tot = 0
found = False
if "etherStatsJabbers" in rmon_in:
tot += rmon_in['etherStatsJabbers']
found = True
if "etherStatsOversizePkts" in rmon_in:
tot += rmon_in['etherStatsOversizePkts']
found = True
if found:
frame['in-error-oversize-frames'] = str(tot)
def add_ethtool_std(ifname, iface_out):
"""Fetch interface speed/duplex/autoneg from kernel"""
keys = ['Speed', 'Duplex', 'Auto-negotiation']
result = {}
lines = HOST.run_multiline(['ethtool', ifname])
for line in lines:
line = line.strip()
key = line.split(':', 1)[0].strip()
if key in keys:
key, value = line.split(':', 1)
result[key.strip()] = value.strip()
if "Auto-negotiation" in result:
if result['Auto-negotiation'] == "on":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "auto-negotiation", "enable", True)
else:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "auto-negotiation", "enable", False)
if "Duplex" in result:
if result['Duplex'] == "Half":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "half")
elif result['Duplex'] == "Full":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "full")
else:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "unknown")
if "Speed" in result and result['Speed'] != "Unknown!":
# Avoid importing re (performance)
num = ''.join(filter(str.isdigit, result['Speed']))
if num:
num = round((int(num) / 1000), 3)
insert(iface_out, "ieee802-ethernet-interface:ethernet", "speed", str(num))
def get_querier_data(querier_data):
multicast={}
if not querier_data:
multicast["snooping"] = False
return multicast
multicast["snooping"] = True
if(querier_data.get("query-interval")):
multicast["query-interval"] = querier_data["query-interval"]
return multicast
def get_multicast_filters(filters):
multicast_filters=[]
for f in filters:
multicast_filter={}
multicast_filter["group"] = f["group"]
multicast_filter["ports"] = []
for p in f["ports"]:
port={}
port["port"] = p
multicast_filter["ports"].append(port)
multicast_filters.append(multicast_filter)
return multicast_filters
def add_mdb_to_bridge(brname, iface_out, mc_status):
filters = [entry for entry in mc_status.get("multicast-groups", []) if entry.get('vid') == None and entry.get('bridge') == brname]
querier = next((querier for querier in mc_status.get('multicast-queriers', []) if (querier.get("interface", "") == brname) and (querier.get("vid") is None)), None)
multicast = get_querier_data(querier)
multicast_filters = get_multicast_filters(filters)
insert(iface_out, "infix-interfaces:bridge", "multicast", multicast)
insert(iface_out, "infix-interfaces:bridge", "multicast-filters", "multicast-filter", multicast_filters)
def add_container_ifaces(yang_ifaces):
"""Add all podman interfaces with limited data"""
interfaces={}
try:
containers = HOST.run_json(['podman', 'ps', '-a', '--format=json'], default=[])
except Exception as e:
logging.error(f"Error, unable to run podman: {e}")
return
for container in containers:
name = container.get('Names', ['Unknown'])[0]
networks = container.get('Networks', [])
for network in networks:
if not network in interfaces:
interfaces[network] = []
if name not in interfaces[network]:
interfaces[network].append(name)
for ifname, containers in interfaces.items():
iface_out = {}
iface_out['name'] = ifname
iface_out['type'] = "infix-if-type:other" # Fallback
insert(iface_out, "infix-interfaces:container-network", "containers", containers)
netns = netns_find_ifname(ifname)
if netns is not None:
ip_link_data = netns_get_ip_link(netns)
ip_link_data = next((d for d in ip_link_data if d.get('ifalias') == ifname), None)
add_ip_link(ifname, ip_link_data, iface_out)
yang_ifaces.append(iface_out)
# Helper function to add tagged/untagged interfaces to a vlan dict in a list
def _add_vlan_iface(vlans, multicast_filter, multicast, vid, key, val):
for d in vlans:
if d['vid'] == vid:
if key in d:
d[key].append(val)
else:
d[key] = [val]
return
vlans.append({'vid': vid, "multicast-filters": {"multicast-filter": multicast_filter}, "multicast": multicast, key: [val]})
def add_vlans_to_bridge(brname, iface_out, mc_status):
slaves = [] # Contains all interfaces that has this bridge as 'master'
for iface in HOST.run_json(['bridge', '-j', 'link']):
if "master" in iface and iface['master'] == brname:
slaves.append(iface['ifname'])
vlans = [] # Contains all vlans and slaves belonging to this bridge
for iface in HOST.run_json(['bridge', '-j', 'vlan']):
if iface['ifname'] not in slaves and iface['ifname'] != brname:
continue
for vlan in iface['vlans']:
querier = next((querier for querier in mc_status.get('multicast-queriers', []) if (querier.get("vid") == vlan['vlan'])), None)
filters = [entry for entry in mc_status.get("multicast-groups", []) if (entry.get("vid") == vlan["vlan"])]
if 'flags' in vlan and 'Egress Untagged' in vlan['flags']:
_add_vlan_iface(vlans, get_multicast_filters(filters), get_querier_data(querier), vlan['vlan'], 'untagged', iface['ifname'])
else:
_add_vlan_iface(vlans, get_multicast_filters(filters), get_querier_data(querier), vlan['vlan'], 'tagged', iface['ifname'])
insert(iface_out, "infix-interfaces:bridge", "vlans", "vlan", vlans)
def get_iface_data(ifname, ip_link_data, ip_addr_data):
iface_out = {}
add_ip_link(ifname, ip_link_data, iface_out)
add_ip_addr(ifname, ip_addr_data, iface_out)
if 'type' in iface_out and iface_out['type'] == "infix-if-type:ethernet":
add_ethtool_groups(ifname, iface_out)
add_ethtool_std(ifname, iface_out)
if 'type' in iface_out and iface_out['type'] == "infix-if-type:bridge":
# Fail silent, multicast snooping may not be enabled on bridge
mc_status = HOST.run_json(['mctl', '-p', 'show', 'igmp', 'json'], default={})
add_vlans_to_bridge(ifname, iface_out, mc_status)
add_mdb_to_bridge(ifname, iface_out, mc_status)
return iface_out
def _add_interface(ifname, ip_link_data, ip_addr_data, yang_ifaces):
# We expect both ip addr and link data to exist.
if not ip_link_data or not ip_addr_data:
return
# Skip internal interfaces.
if 'group' in ip_link_data and ip_link_data['group'] == "internal":
return
yang_ifaces.append(get_iface_data(ifname, ip_link_data, ip_addr_data))
def operational(ifname=None):
out = {
"ietf-interfaces:interfaces": {
"interface": []
}
}
out_ifaces = out["ietf-interfaces:interfaces"]["interface"]
ip_link_data = get_ip_link()
ip_addr_data = get_ip_addr()
if ifname:
ip_link_data = next((d for d in ip_link_data if d.get('ifname') == ifname), None)
ip_addr_data = next((d for d in ip_addr_data if d.get('ifname') == ifname), None)
_add_interface(ifname, ip_link_data, ip_addr_data, out_ifaces)
else:
for link in ip_link_data:
addr = next((d for d in ip_addr_data if d.get('ifname') == link["ifname"]), None)
_add_interface(link["ifname"], link, addr, out_ifaces)
add_container_ifaces(out_ifaces)
return out
+156
View File
@@ -0,0 +1,156 @@
from .common import insert
from .host import HOST
def frr_to_ietf_neighbor_state(state):
"""Fetch OSPF neighbor state from Frr"""
state = state.split("/")[0]
if state == "TwoWay":
return "2-way"
return state.lower()
def add_routes(ospf):
"""Fetch OSPF routes from Frr"""
cmd = ['vtysh', '-c', "show ip ospf route json"]
data = HOST.run_json(cmd, default=[])
if data == []:
return # No OSPF routes available
routes = []
for prefix, info in data.items():
if prefix.find("/") == -1: # Ignore router IDs
continue
route = {}
route["prefix"] = prefix
nexthops = []
routetype = info["routeType"].split(" ")
if len(routetype) > 1:
if routetype[1] == "E1":
route["route-type"] = "external-1"
elif routetype[1] == "E2":
route["route-type"] = "external-2"
elif routetype[1] == "IA":
route["route-type"] = "inter-area"
elif routetype[0] == "N":
route["route-type"] = "intra-area"
for hop in info["nexthops"]:
nexthop = {}
if hop["ip"] != " ":
nexthop["next-hop"] = hop["ip"]
else:
nexthop["outgoing-interface"] = hop["directlyAttachedTo"]
nexthops.append(nexthop)
route["next-hops"] = {}
route["next-hops"]["next-hop"] = nexthops
routes.append(route)
insert(ospf, "ietf-ospf:local-rib", "ietf-ospf:route", routes)
def add_areas(control_protocols):
"""Populate OSPF status"""
cmd = ['/usr/libexec/statd/ospf-status']
data = HOST.run_json(cmd, default={})
if data == {}:
return # No OSPF data available
control_protocol = {}
control_protocol["type"] = "infix-routing:ospfv2"
control_protocol["name"] = "default"
control_protocol["ietf-ospf:ospf"] = {}
control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"] = {}
control_protocol["ietf-ospf:ospf"]["ietf-ospf:router-id"] = data.get("routerId")
control_protocol["ietf-ospf:ospf"]["ietf-ospf:address-family"] = "ipv4"
areas = []
for area_id, values in data.get("areas", {}).items():
area = {}
area["ietf-ospf:area-id"] = area_id
area["ietf-ospf:interfaces"] = {}
if values.get("area-type"):
area["ietf-ospf:area-type"] = values["area-type"]
interfaces = []
for iface in values.get("interfaces", {}):
interface = {}
interface["ietf-ospf:neighbors"] = {}
interface["name"] = iface["name"]
if iface.get("drId"):
interface["dr-router-id"] = iface["drId"]
if iface.get("drAddress"):
interface["dr-ip-addr"] = iface["drAddress"]
if iface.get("bdrId"):
interface["bdr-router-id"] = iface["bdrId"]
if iface.get("bdrAddress"):
interface["bdr-ip-addr"] = iface["bdrAddress"]
if iface.get("timerPassiveIface"):
interface["passive"] = True
else:
interface["passive"] = False
interface["enabled"] = iface["ospfEnabled"]
if iface["networkType"] == "POINTOPOINT":
interface["interface-type"] = "point-to-point"
if iface["networkType"] == "BROADCAST":
interface["interface-type"] = "broadcast"
if iface.get("state"):
# Wev've never seen "DependUpon", and has no entry in
# the YANG model, but is listed before down in Frr
xlate = {
"DependUpon": "down",
"Down": "down",
"Waiting": "waiting",
"Loopback": "loopback",
"Point-To-Point": "point-to-point",
"DROther": "dr-other",
"Backup": "bdr",
"DR": "dr"
}
val = xlate.get(iface["state"], "unknown")
interface["state"] = val
neighbors = []
for neigh in iface["neighbors"]:
neighbor = {}
neighbor["neighbor-router-id"] = neigh["neighborIp"]
neighbor["address"] = neigh["ifaceAddress"]
neighbor["dead-timer"] = neigh["routerDeadIntervalTimerDueMsec"]
neighbor["state"] = frr_to_ietf_neighbor_state(neigh["nbrState"])
if neigh.get("routerDesignatedId"):
neighbor["dr-router-id"] = neigh["routerDesignatedId"]
if neigh.get("routerDesignatedBackupId"):
neighbor["bdr-router-id"] = neigh["routerDesignatedBackupId"]
neighbors.append(neighbor)
interface["ietf-ospf:neighbors"] = {}
interface["ietf-ospf:neighbors"]["ietf-ospf:neighbor"] = neighbors
interfaces.append(interface)
area["ietf-ospf:interfaces"]["ietf-ospf:interface"] = interfaces
areas.append(area)
add_routes(control_protocol["ietf-ospf:ospf"])
control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"]["ietf-ospf:area"] = areas
insert(control_protocols, "control-plane-protocol", [control_protocol])
def operational():
out = {
"ietf-routing:routing": {
"control-plane-protocols": {
}
}
}
add_areas(out['ietf-routing:routing']['control-plane-protocols'])
return out
+149
View File
@@ -0,0 +1,149 @@
from datetime import timedelta
from re import match
from .common import insert
from .host import HOST
def uptime2datetime(uptime):
"""
Convert uptime to YANG format (YYYY-MM-DDTHH:MM:SS+00:00)
Handles the following input formats (frrtime):
HH:MM:SS
XdXXhXXm
XXwXdXXh
"""
h = m = s = 0
# Format HH:MM:SS
if match(r'^\d{2}:\d{2}:\d{2}$', uptime):
h, m, s = map(int, uptime.split(':'))
# Format XdXXhXXm (days, hours, minutes)
elif match(r'^\d+d\d{2}h\d{2}m$', uptime):
days = int(uptime.split('d')[0])
h = int(uptime.split('d')[1].split('h')[0])
m = int(uptime.split('h')[1].split('m')[0])
h += days * 24
# Format XwXdXXh (weeks, days, hours)
elif match(r'^\d{2}w\d{1}d\d{2}h$', uptime):
weeks = int(uptime.split('w')[0])
days = int(uptime.split('w')[1].split('d')[0])
h = int(uptime.split('d')[1].split('h')[0])
h += weeks * 7 * 24
h += days * 24
uptime_delta = timedelta(hours=h, minutes=m, seconds=s)
current_time = HOST.now()
last_updated = current_time - uptime_delta
date_timestd = last_updated.strftime('%Y-%m-%dT%H:%M:%S%z')
return date_timestd[:-2] + ':' + date_timestd[-2:]
def add_protocol(routes, proto):
"""Populate routes from vtysh JSON output"""
frrproto = "ip" if proto == "ipv4" else proto
data = HOST.run_json(['vtysh', '-c', f"show {frrproto} route json"], {})
# Mapping of FRR protocol names to IETF routing-protocol
pmap = {
'kernel': 'infix-routing:kernel',
'connected': 'direct',
'static': 'static',
'ospf': 'ietf-ospf:ospfv2',
'ospf6': 'ietf-ospf:ospfv3',
}
out = {}
out["route"] = []
if proto == "ipv4":
default = "0.0.0.0/0"
host_prefix_length = "32"
else:
default = "::/0"
host_prefix_length = "128"
for prefix, entries in data.items():
for route in entries:
new = {}
dst = route.get('prefix', default)
if '/' not in dst:
dst = f"{dst}/{route.get('prefixLen', host_prefix_length)}"
new[f'ietf-{proto}-unicast-routing:destination-prefix'] = dst
frr = route.get('protocol', 'infix-routing:kernel')
new['source-protocol'] = pmap.get(frr, 'infix-routing:kernel')
new['route-preference'] = route.get('distance', 0)
# Metric only available in the model for OSPF routes
if 'ospf' in frr:
new['ietf-ospf:metric'] = route.get('metric', 0)
# See https://datatracker.ietf.org/doc/html/rfc7951#section-6.9
# for details on how presence leaves are encoded in JSON: [null]
if route.get('selected', False):
new['active'] = [None]
new['last-updated'] = uptime2datetime(route.get('uptime', 0))
installed = route.get('installed', False)
next_hops = []
for hop in route.get('nexthops', []):
next_hop = {}
if hop.get('ip'):
next_hop[f'ietf-{proto}-unicast-routing:address'] = hop['ip']
elif hop.get('interfaceName'):
next_hop['outgoing-interface'] = hop['interfaceName']
# See zebra/zebra_vty.c:re_status_outpupt_char()
if installed and hop.get('fib', False):
next_hop['infix-routing:installed'] = [None]
next_hops.append(next_hop)
if next_hops:
new['next-hop'] = {'next-hop-list': {'next-hop': next_hops}}
else:
next_hop = {}
protocol = route.get('protocol', 'unicast')
if protocol == "blackhole":
next_hop['special-next-hop'] = "blackhole"
elif protocol == "unreachable":
next_hop['special-next-hop'] = "unreachable"
else:
if route.get('interfaceName'):
next_hop['outgoing-interface'] = route['interfaceName']
if route.get('nexthop'):
next_hop[f'ietf-{proto}-unicast-routing:next-hop-address'] = route['nexthop']
new['next-hop'] = next_hop
out['route'].append(new)
insert(routes, 'routes', out)
def operational():
out = {
"ietf-routing:routing": {
"ribs": {
"rib": [{
"name": "ipv4",
"address-family": "ipv4"
}, {
"name": "ipv6",
"address-family": "ipv6"
}]
}
}
}
ipv4routes = out['ietf-routing:routing']['ribs']['rib'][0]
ipv6routes = out['ietf-routing:routing']['ribs']['rib'][1]
add_protocol(ipv4routes, "ipv4")
add_protocol(ipv6routes, "ipv6")
return out
+139
View File
@@ -0,0 +1,139 @@
import subprocess
from .common import insert
from .host import HOST
def uboot_get_boot_order():
data = HOST.run_multiline("fw_printenv BOOT_ORDER".split(), [])
for line in data:
if "BOOT_ORDER" in line:
return line.strip().split("=")[1].split()
raise Exception
def grub_get_boot_order():
data = HOST.run_multiline("grub-editenv /mnt/aux/grub/grubenv list".split(), [])
for line in data:
if "ORDER" in line:
return line.split("=")[1].strip().split()
raise Exception
def get_boot_order():
order = None
try:
order = uboot_get_boot_order()
except:
pass
try:
if order is None:
order = grub_get_boot_order()
except:
pass
return order
def add_ntp(out):
data = HOST.run_multiline(["chronyc", "-c", "sources"], [])
source = []
state_mode_map = {
"^": "server",
"=": "peer",
"#": "local-clock"
}
source_state_map = {
"*": "selected",
"+": "candidate",
"-": "outlier",
"?": "unusable",
"x": "falseticker",
"~": "unstable"
}
for line in data:
src = {}
line = line.split(',')
src["address"] = line[2]
src["mode"] = state_mode_map[line[0]]
src["state"] = source_state_map[line[1]]
src["stratum"] = int(line[3])
src["poll"] = int(line[4])
source.append(src)
insert(out, "infix-system:ntp", "sources", "source", source)
def add_software_slots(out, data):
slots = []
for slot in data["slots"]:
for key, value in slot.items():
new = {}
new["name"] = key
new["bootname"] = slot[key].get("bootname")
new["class"] = slot[key].get("class")
new["state"] = slot[key].get("state")
new["bundle"] = {}
slot_status=value.get("slot_status", {})
if slot_status.get("bundle", {}).get("compatible"):
new["bundle"]["compatible"] = slot_status.get("bundle", {}).get("compatible")
if slot_status.get("bundle", {}).get("version"):
new["bundle"]["version"] = slot_status.get("bundle", {}).get("version")
if slot_status.get("checksum", {}).get("size"):
new["size"] = str(slot_status.get("checksum", {}).get("size"))
if slot_status.get("checksum", {}).get("sha256"):
new["sha256"] = slot_status.get("checksum", {}).get("sha256")
new["installed"] = {}
if slot_status.get("installed", {}).get("timestamp"):
new["installed"]["datetime"] = slot_status.get("installed", {}).get("timestamp")
if slot_status.get("installed", {}).get("count"):
new["installed"]["count"] = slot_status.get("installed", {}).get("count")
new["activated"] = {}
if slot_status.get("activated", {}).get("timestamp"):
new["activated"]["datetime"] = slot_status.get("activated", {}).get("timestamp")
if slot_status.get("activated", {}).get("count"):
new["activated"]["count"] = slot_status.get("activated", {}).get("count")
slots.append(new)
out["slot"] = slots
def add_software(out):
software = {}
try:
data = HOST.run_json(["rauc", "status", "--detailed", "--output-format=json"])
software["compatible"] = data["compatible"]
software["variant"] = data["variant"]
software["booted"] = data["booted"]
boot_order = get_boot_order()
if not boot_order is None:
software["boot-order"] = boot_order
add_software_slots(software, data)
except subprocess.CalledProcessError:
pass # Maybe an upgrade i progress, then rauc does not respond
installer_status = HOST.run_json(["rauc-installation-status"])
installer = {}
if installer_status.get("operation"):
installer["operation"] = installer_status["operation"]
if "progress" in installer_status:
progress = {}
if installer_status["progress"].get("percentage"):
progress["percentage"] = int(installer_status["progress"]["percentage"])
if installer_status["progress"].get("message"):
progress["message"] = installer_status["progress"]["message"]
installer["progress"] = progress
software["installer"] = installer
insert(out, "infix-system:software", software)
def operational():
out = {
"ietf-system:system-state": {
}
}
out_state = out["ietf-system:system-state"]
add_software(out_state)
add_ntp(out_state)
return out
@@ -0,0 +1,76 @@
from .common import LOG
from .host import HOST
def podman_inspect(name):
"""Call podman inspect {name}, return object at {path} or None."""
cmd = ['podman', 'inspect', name]
try:
return HOST.run_json(cmd, default=[])
except Exception as e:
LOG(f"Error running podman inspect: {e}")
return []
def podman_ps():
"""We list *all* containers, not just those in the configuraion."""
return HOST.run_json("podman ps -a --format=json".split(), default=[])
def network(ps, inspect):
net = {}
# The 'podman ps' command lists ports even in host mode, but
# that's not applicable, so skip networks and port forwardings
networks = inspect.get("NetworkSettings", {}).get("Networks")
if networks and "host" in networks:
net = {"host": True}
else:
net = {
"interface": [{"name": net} for net in ps["Networks"]],
"publish": []
}
if (ps["State"] == "running") and ps["Ports"]:
for port in ps["Ports"]:
addr = ""
if port["host_ip"]:
addr = f"{port['host_ip']}:"
pub = f"{addr}{port['host_port']}->{port['container_port']}/{port['protocol']}"
net["publish"].append(pub)
return net
def container(ps):
out = {
"name": ps["Names"][0],
"id": ps["Id"],
"image": ps["Image"],
"image-id": ps["ImageID"],
"running": ps["State"] == "running",
"status": ps["Status"]
}
# Bonus information, may not be available
if ps["Command"]:
out["command"] = " ".join(ps["Command"])
inspect = podman_inspect(out["name"])
if inspect and isinstance(inspect, list) and len(inspect) > 0:
inspect = inspect[0]
else:
inspect = {}
net = network(ps, inspect)
if net:
out["network"] = net
return out
def operational():
return {
"infix-containers:containers": {
"container": [container(ps) for ps in podman_ps()]
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
exec env PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=$(readlink -f $(dirname "$0")/../) \
python3 -m yanger "$@"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@
SCRIPT_PATH="$(dirname "$(readlink -f "$0")")"
ROOT_PATH="$SCRIPT_PATH/../../../"
YANGER_TOOL="$ROOT_PATH/src/statd/python/yanger/yanger.py"
YANGER_TOOL="$ROOT_PATH/src/statd/python/yanger/yanger"
INTERFACES_OUTPUT_FILE="$(mktemp)"
ROUTES_OUTPUT_FILE="$(mktemp)"
@@ -0,0 +1 @@
{"vendor": "QEMU", "product-name": "VM", "part-number": null, "serial-number": null, "mac-address": "02:00:00:00:00:00", "factory-password-hash": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A", "vpd": {"product": {"board": "product", "available": false, "trusted": true, "data": {}}}, "usb-ports": [{"name": "USB", "path": "/sys/bus/usb/devices/usb1/authorized"}, {"name": "USB", "path": "/sys/bus/usb/devices/usb1/authorized_default"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2/authorized"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2/authorized_default"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"10.0.0.1/32":{"routeType":"N","transit":false,"cost":0,"area":"0.0.0.1","nexthops":[{"ip":" ","directlyAttachedTo":"lo"}]},"10.0.0.2/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"10.0.0.3/32":{"routeType":"N","transit":false,"cost":2000,"area":"0.0.0.1","nexthops":[{"ip":"10.0.13.2","via":"e6","advertisedRouter":"10.0.0.3"}]},"10.0.0.4/32":{"routeType":"N IA","cost":2001,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"10.0.12.0/30":{"routeType":"N","transit":true,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":" ","directlyAttachedTo":"e5"}]},"10.0.13.0/30":{"routeType":"N","transit":true,"cost":2000,"area":"0.0.0.1","nexthops":[{"ip":" ","directlyAttachedTo":"e6"}]},"10.0.23.0/30":{"routeType":"N","transit":true,"cost":2001,"area":"0.0.0.1","nexthops":[{"ip":"10.0.13.2","via":"e6","advertisedRouter":"10.0.0.3"}]},"10.0.24.0/30":{"routeType":"N IA","cost":2001,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"10.0.41.0/30":{"routeType":"N IA","cost":2002,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.8.1/32":{"routeType":"N","transit":false,"cost":0,"area":"0.0.0.1","nexthops":[{"ip":" ","directlyAttachedTo":"lo"}]},"11.0.9.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.10.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.11.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.12.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.13.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.14.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"11.0.15.1/32":{"routeType":"N","transit":false,"cost":1,"area":"0.0.0.0","nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]},"192.168.3.0/24":{"routeType":"N","transit":false,"cost":2001,"area":"0.0.0.1","nexthops":[{"ip":"10.0.13.2","via":"e6","advertisedRouter":"10.0.0.3"}]},"1.1.1.1":{"routeType":"R ","cost":1,"area":"0.0.0.0","routerType":"abr","nexthops":[{"ip":"10.0.12.2","via":"e5"}]},"10.0.0.4":{"routeType":"R ","cost":2001,"area":"0.0.0.0","IA":true,"ia":true,"routerType":"asbr","nexthops":[{"ip":"10.0.12.2","via":"e5"}]},"192.168.4.0/24":{"routeType":"N E2","cost":2001,"type2cost":20,"tag":0,"nexthops":[{"ip":"10.0.12.2","via":"e5","advertisedRouter":"1.1.1.1"}]}}
Executable
+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
ixdir=$(readlink -f $(dirname "$0")/../)
. "$ixdir/utils/libix.sh"
resolve_host_dir
YANGLINT="$HOST_DIR/bin/yanglint"
[ -x "$YANGLINT" ] || {
echo "*** Error: yanglint is not installed" >&2
exit 1
}
yangdir="$ixdir/src/confd/yang"
check()
{
find "$yangdir" -type f -name '*infix*.yang' | while read file; do
head -n1 "$file" | grep -q "submodule" && continue
printf "%-40s " $(basename "$file")
"$YANGLINT" -p "$yangdir" -i "$file" || {
printf "%-40s FAIL\n" $(basename "$file")
continue
}
echo ok
done
}
resolve_model()
{
local model="$1"
if [ -e "$model" ]; then
file="$model"
elif [ -e "$yangdir/$model" ]; then
file="$yangdir/$model"
elif [ -e "$yangdir/$model.yang" ]; then
file="$yangdir/$model.yang"
else
file=$(find "$yangdir" -name "$model*" -print -quit)
fi
if [ -z "$file" ]; then
echo "*** Error: Found no model matching \"$model\"" >&2
exit 1
fi
echo "$file"
}
lint()
{
local files=
while [ $# -gt 0 ]; do
if [ "$1" = "--" ]; then
shift
break
fi
files="$files $(resolve_model $1)"
shift
done
"$YANGLINT" -p "$yangdir" -i "$@" $files
}
yanger()
{
local model="$1"
"$ixdir/src/statd/python/yanger/yanger" \
-t "$ixdir/test/case/cli/system-output" \
"$model"
}
yangerlint()
{
local model="$1"
yanger "$model" >/tmp/ixyang-yangerlint.json || {
echo "*** Error: Failed to dump \"$model\"" >&2
exit 1
}
case "$model" in
ietf-interfaces)
lint \
ietf-interfaces \
ieee802-ethernet-interface \
infix-interfaces \
infix-ethernet-interface \
\
/tmp/ixyang-yangerlint.json \
-- -i -t data
;;
*)
echo "*** Error: yangerlint not implemented for \"$model\"" >&2
;;
esac
rm /tmp/ixyang-yangerlint.json
}
case "$1" in
check|lint|yanger|yangerlint)
"$@"
;;
*)
echo "*** Error: Unknown command \"$1\"" >&2
return 1
esac
+33
View File
@@ -0,0 +1,33 @@
resolve_o()
{
[ -n "$O" ] && return
if [ -f ".config" ] && [ -d "output" ]; then
# Buildroot
O=$(readlink -f ./output)
elif [ -f "output/.config" ]; then
# BR2_EXTERNAL
O=$(readlink -f ./output)
elif [ -f ".config" ] && [ -d "host" ]; then
# Called from inside output/ directory
O=$(readlink -f .)
else
echo "*** Error: cannot find Buildroot output dir!" >&2
exit 1
fi
}
resolve_host_dir()
{
[ -n "$HOST_DIR" ] && return
resolve_o
if ! [ -d "$O/host" ]; then
echo "*** Error: cannot find Buildroot host binaries dir!" >&2
exit 1
fi
HOST_DIR="$O/host"
}
+3 -23
View File
@@ -1,6 +1,8 @@
#!/bin/sh
# Interact with sysrepo in a Buildroot setup
. $(dirname $(readlink -f "$0"))/libix.sh
usage()
{
cat <<EOF
@@ -30,29 +32,7 @@ cleanup()
exit 1
}
# Figure out if we're called from output/ or top directory
if [ -z "$O" ]; then
if [ -f ".config" ] && [ -d "output" ]; then
# Buildroot
O=./output
elif [ -f "output/.config" ]; then
# BR2_EXTERNAL
O=./output
elif [ -f ".config" ] && [ -d "host" ]; then
# Called from inside output/ directory
O=.
else
echo "*** Error: cannot find Buildroot output dir!" >&2
exit 1
fi
fi
if [ -d "$O/host" ]; then
HOST_DIR="$O/host"
else
echo "*** Error: cannot find Buildroot host binaries dir!" >&2
exit 1
fi
resolve_host_dir
MOD="$O/target/usr/share/yang/modules/confd/"
CTL="$HOST_DIR/bin/sysrepoctl"