diff --git a/lib/netconf-junos/default.nix b/lib/netconf-junos/default.nix
new file mode 100644
index 0000000..142c45c
--- /dev/null
+++ b/lib/netconf-junos/default.nix
@@ -0,0 +1,74 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib) mapAttrs mkOption;
+
+ inherit (lib.types)
+ attrsOf
+ bool
+ str
+ submodule
+ ;
+
+ mandatory.options = {
+ supportPoE = mkOption {
+ type = bool;
+ example = true;
+ description = ''
+ Wether this interface supports PoE.
+ '';
+ };
+ };
+in
+{
+ imports = [
+ ./interfaces.nix
+ ./poe.nix
+ ./protocols.nix
+ ./system.nix
+ ./vlans.nix
+ ];
+
+ options = {
+ netconf.xmls.configuration = mkOption {
+ type = str;
+ readOnly = true;
+ description = ''
+ The full configuration to send to a JunOS.
+ '';
+ };
+ netconf.mandatoryInterfaces = mkOption {
+ type = attrsOf (submodule mandatory);
+ example = {
+ "ge-0/0/0" = {
+ supportPoE = true;
+ };
+ "ge-0/0/1" = {
+ supportPoE = true;
+ };
+ "xe-0/0/0" = {
+ supportPoE = false;
+ };
+ };
+ description = ''
+ JunOS require some interfaces to always be configured (even if they are disabled),
+ which correspond to physical interfaces of the switch. They have to be declared here
+ with some information about it (only if it supports PoE for now).
+ '';
+ };
+ };
+ config.interfaces =
+ let
+ mkIntf = _: _: { };
+ in
+ mapAttrs mkIntf config.netconf.mandatoryInterfaces;
+ config.netconf.xmls.configuration = with config.netconf.xmls; ''
+
+ ${system}
+ ${interfaces}
+ ${protocols}
+ ${vlans}
+ ${poe}
+
+ '';
+}
diff --git a/lib/netconf-junos/interfaces.nix b/lib/netconf-junos/interfaces.nix
new file mode 100644
index 0000000..69b3af0
--- /dev/null
+++ b/lib/netconf-junos/interfaces.nix
@@ -0,0 +1,168 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib)
+ mapAttrsToList
+ mkEnableOption
+ mkOption
+ optionalString
+ ;
+
+ inherit (lib.types)
+ attrsOf
+ either
+ enum
+ ints
+ listOf
+ str
+ submodule
+ ;
+
+ interface =
+ { name, config, ... }:
+ let
+ unit =
+ { name, config, ... }:
+ {
+ options = {
+ enable = mkEnableOption "this logical interface" // {
+ default = true;
+ example = false;
+ };
+ family = {
+ ethernet-switching = {
+ enable = mkEnableOption "the ethernet switching on this logical interface";
+ interface-mode = mkOption {
+ type = enum [
+ "trunk"
+ "access"
+ ];
+ description = ''
+ Mode of operation for vlan addressing of this interface.
+ "trunk" means that the traffic is tagged, "access" means the
+ traffic is tagged by the switch.
+ '';
+ };
+ vlans = mkOption {
+ type = listOf (either str ints.unsigned);
+ default = [ ];
+ description = ''
+ Vlans that can be used on this interface.
+ Only one ID should be here for "access" mode of operation.
+ '';
+ };
+ };
+ #TODO : DHCP
+ inet = {
+ enable = mkEnableOption "the IPv4 configuration of this logical interface";
+ addresses = mkOption {
+ type = listOf str;
+ default = [ ];
+ description = ''
+ ipv4 addresses of this interface.
+ '';
+ };
+ };
+ inet6 = {
+ enable = mkEnableOption "the IPv6 configuration of this logical interface";
+ addresses = mkOption {
+ type = listOf str;
+ default = [ ];
+ description = ''
+ ipv6 addresses of this interface.
+ '';
+ };
+ };
+ };
+ xml = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.xml =
+ let
+ members = map (
+ vlan: "${builtins.toString vlan}"
+ ) config.family.ethernet-switching.vlans;
+ eth = optionalString config.family.ethernet-switching.enable ''
+
+ ${config.family.ethernet-switching.interface-mode}
+ ${builtins.concatStringsSep "" members}
+ default
+
+ '';
+
+ addr4 = map (addr: "${addr}") config.family.inet.addresses;
+ inet = optionalString config.family.inet.enable ''
+
+ ${builtins.concatStringsSep "" addr4}
+
+ '';
+
+ addr6 = map (addr: "${addr}") config.family.inet6.addresses;
+ inet6 = optionalString config.family.inet6.enable ''
+
+ ${builtins.concatStringsSep "" addr6}
+
+ '';
+ in
+ ''
+
+ ${name}
+ ${optionalString (!config.enable) ""}
+
+ ${eth}${inet}${inet6}
+
+ '';
+ };
+ in
+ {
+ options = {
+ enable = mkEnableOption "this physical interface";
+ unit = mkOption {
+ type = attrsOf (submodule unit);
+ default = { };
+ description = ''
+ Configuration of the logical interfaces on this physical interface.
+ '';
+ };
+ xml = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.xml =
+ let
+ units = mapAttrsToList (_: unit: unit.xml) config.unit;
+ in
+ ''
+
+ ${name}
+ ${optionalString (!config.enable) ""}
+ ${builtins.concatStringsSep "" units}
+
+ '';
+ };
+in
+{
+ options = {
+ interfaces = mkOption {
+ type = attrsOf (submodule interface);
+ description = ''
+ The interfaces configuration.
+ '';
+ };
+ netconf.xmls.interfaces = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.netconf.xmls.interfaces = ''
+
+ ${builtins.concatStringsSep "" (mapAttrsToList (_: intf: intf.xml) config.interfaces)}
+
+ '';
+}
diff --git a/lib/netconf-junos/poe.nix b/lib/netconf-junos/poe.nix
new file mode 100644
index 0000000..c1b4bbc
--- /dev/null
+++ b/lib/netconf-junos/poe.nix
@@ -0,0 +1,49 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib)
+ mapAttrsToList
+ mkEnableOption
+ mkOption
+ optionalString
+ ;
+
+ inherit (lib.types) attrsOf str submodule;
+
+ interface-module =
+ { name, config, ... }:
+ {
+ options = {
+ enable = mkEnableOption "the PoE for this interface";
+ xml = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.xml = ''
+ ${name}${optionalString (!config.enable) ""}
+ '';
+ };
+in
+{
+ options = {
+ poe.interfaces = mkOption {
+ type = attrsOf (submodule interface-module);
+ default = { };
+ description = ''
+ PoE configuration of interfaces.
+ '';
+ };
+ netconf.xmls.poe = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.netconf.xmls.poe = ''
+
+ ${builtins.concatStringsSep "" (mapAttrsToList (_: intf: intf.xml) config.poe.interfaces)}
+
+ '';
+}
diff --git a/lib/netconf-junos/protocols.nix b/lib/netconf-junos/protocols.nix
new file mode 100644
index 0000000..6feac1e
--- /dev/null
+++ b/lib/netconf-junos/protocols.nix
@@ -0,0 +1,33 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib) concatMapStringsSep mkOption;
+
+ inherit (lib.types) listOf str;
+in
+
+{
+ options = {
+ protocols.rstp = mkOption {
+ type = listOf str;
+ description = ''
+ List of interfaces on which Rapid Spanning Tree Protocol should be enabled.
+ '';
+ };
+ netconf.xmls.protocols = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+
+ config.netconf.xmls.protocols = ''
+
+
+ ${
+ concatMapStringsSep "" (intf: "${intf}") config.protocols.rstp
+ }
+
+
+ '';
+}
diff --git a/lib/netconf-junos/system.nix b/lib/netconf-junos/system.nix
new file mode 100644
index 0000000..5045481
--- /dev/null
+++ b/lib/netconf-junos/system.nix
@@ -0,0 +1,91 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib)
+ concatStrings
+ concatStringsSep
+ filter
+ hasPrefix
+ length
+ mkOption
+ splitString
+ ;
+
+ inherit (lib.types)
+ enum
+ listOf
+ port
+ str
+ ;
+in
+
+{
+ options = {
+ system = {
+ host-name = mkOption {
+ type = str;
+ description = "The hostname of the switch.";
+ };
+ root-authentication = {
+ hashedPasswd = mkOption {
+ type = str;
+ description = "Hashed password for root.";
+ };
+ ssh-keys = mkOption {
+ type = listOf str;
+ description = "ssh keys for root user.";
+ default = [ ];
+ };
+ };
+ services = {
+ ssh.root-login = mkOption {
+ type = enum [
+ "allow"
+ "deny"
+ "deny-password"
+ ];
+ description = "Login policy to use for root.";
+ };
+ netconf.port = mkOption {
+ type = port;
+ description = "Port to use for netconf.";
+ default = 830;
+ };
+ };
+ };
+ netconf.xmls.system = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.netconf.xmls.system =
+ let
+ ssh-keys1 = map (splitString " ") config.system.root-authentication.ssh-keys;
+ ssh-keys2 = map (key: if length key < 3 then key ++ [ "foo@bar" ] else key) ssh-keys1;
+ ssh-keys = map (concatStringsSep " ") ssh-keys2;
+ edsca = map (key: "${key}") (
+ filter (hasPrefix "ssh-edsca ") ssh-keys
+ );
+ rsa = map (key: "${key}") (filter (hasPrefix "ssh-rsa ") ssh-keys);
+ ed25519 = map (key: "${key}") (
+ filter (hasPrefix "ssh-ed25519 ") ssh-keys
+ );
+ in
+ ''
+
+ ${config.system.host-name}
+
+ ${config.system.root-authentication.hashedPasswd}
+ ${concatStrings (edsca ++ rsa ++ ed25519)}
+
+
+ ${config.system.services.ssh.root-login}
+
+ ${toString config.system.services.netconf.port}
+
+
+
+
+ '';
+}
diff --git a/lib/netconf-junos/vlans.nix b/lib/netconf-junos/vlans.nix
new file mode 100644
index 0000000..dedc21e
--- /dev/null
+++ b/lib/netconf-junos/vlans.nix
@@ -0,0 +1,131 @@
+{ config, lib, ... }:
+
+let
+ inherit (lib)
+ assertMsg
+ concatStringsSep
+ mapAttrsToList
+ mkOption
+ optionalString
+ ;
+
+ inherit (lib.types)
+ attrsOf
+ either
+ ints
+ listOf
+ nullOr
+ str
+ submodule
+ unspecified
+ ;
+
+ vlan =
+ { name, config, ... }:
+ {
+ options = {
+ id = mkOption {
+ type = nullOr ints.unsigned;
+ default = null;
+ description = ''
+ The ID of this vlan, `null` means no ID.
+ Incompatible with vlans.${name}.id-list.
+ '';
+ };
+ id-list = mkOption {
+ type =
+ let
+ range_type =
+ { config, ... }:
+ {
+ config.__toString = _: "${toString config.begin}-${toString config.end}";
+ options = {
+ begin = mkOption {
+ type = ints.unsigned;
+ visible = false;
+ };
+ end = mkOption {
+ type = ints.unsigned;
+ visible = false;
+ };
+ __toString = mkOption {
+ visible = false;
+ internal = true;
+ readOnly = true;
+ type = unspecified;
+ };
+ };
+ };
+ in
+ listOf (either ints.unsigned (submodule range_type));
+ default = [ ];
+ example = [
+ 42
+ {
+ begin = 100;
+ end = 200;
+ }
+ ];
+ description = ''
+ List of IDs or IDs range to classify as this vlan.
+ Incompatible with vlans.${name}.id.
+ '';
+ };
+ l3-interface = mkOption {
+ type = nullOr str;
+ default = null;
+ example = "irb.0";
+ description = ''
+ Switch's logical interface to connect directly to this vlan.
+ This allows to communicate with the switch from a vlan without
+ having a cable looping back on it's management interface.
+ '';
+ };
+ xml = mkOption {
+ type = str;
+ readOnly = true;
+ visible = false;
+ };
+ };
+ config.xml =
+ let
+ id = optionalString (config.id != null) "${toString config.id}";
+ id-list = concatStringsSep "" (
+ map (vlan: "${toString vlan}") config.id-list
+ );
+ l3-intf = optionalString (
+ config.l3-interface != null
+ ) "${config.l3-interface}";
+ in
+ assert assertMsg (
+ config.id == null || config.id-list == [ ]
+ ) "vlans.${name}.id and vlans.${name}.id-list are incompatible.";
+ ''
+
+ ${name}
+ ${id}${id-list}${l3-intf}
+
+ '';
+ };
+in
+{
+ options = {
+ vlans = mkOption {
+ type = attrsOf (submodule vlan);
+ description = ''
+ Named vlans configuration. Allows to name vlans inside interface configuration,
+ instead of just using their IDs.
+ '';
+ };
+ netconf.xmls.vlans = mkOption {
+ type = str;
+ visible = false;
+ readOnly = true;
+ };
+ };
+ config.netconf.xmls.vlans = ''
+
+ ${builtins.concatStringsSep "" (mapAttrsToList (_: vlan: vlan.xml) config.vlans)}
+
+ '';
+}