diff options
122 files changed, 11309 insertions, 1589 deletions
diff --git a/.gitignore b/.gitignore index 5d0af4bae..132ee4c13 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ wheels/ .idea/ .idea .idea/* +*.iml # PyInstaller # Usually these files are written by a python script from a template @@ -48,6 +49,7 @@ nosetests.xml coverage.xml *.cover .hypothesis/ +cover # Translations *.mo @@ -9,9 +9,11 @@ interface_definitions: find $(CURDIR)/interface-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1 # XXX: delete top level node.def's that now live in other packages + rm -f $(TMPL_DIR)/interfaces/node.def + rm -f $(TMPL_DIR)/protocols/node.def + rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def rm -f $(TMPL_DIR)/system/options/node.def - rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/vpn/node.def rm -f $(TMPL_DIR)/vpn/ipsec/node.def @@ -23,11 +25,15 @@ op_mode_definitions: find $(CURDIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 # XXX: delete top level op mode node.def's that now live in other packages + rm -f $(OP_TMPL_DIR)/clear/node.def + rm -f $(OP_TMPL_DIR)/set/node.def rm -f $(OP_TMPL_DIR)/show/node.def - rm -f $(OP_TMPL_DIR)/show/dns/node.def + rm -f $(OP_TMPL_DIR)/show/interfaces/node.def + rm -f $(OP_TMPL_DIR)/show/ip/node.def rm -f $(OP_TMPL_DIR)/reset/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def + rm -f $(OP_TMPL_DIR)/generate/node.def .PHONY: all all: clean interface_definitions op_mode_definitions @@ -39,7 +45,7 @@ clean: .PHONY: test test: - PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators --verbose + PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose .PHONY: sonar sonar: diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..5bb9fb73d --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +lxml = "*" +pylint = "*" +nose = "*" +coverage = "*" + +[packages] +vyos = {file = "./python"} +jinja2 = "*" + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..5aaef5675 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,248 @@ +{ + "_meta": { + "hash": { + "sha256": "5564acff86bcf8ef717129935c288ffed2631f1d795d1c44d0af36779593b89b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "index": "pypi", + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, + "vyos": { + "file": "./python" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "coverage": { + "hashes": [ + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "index": "pypi", + "version": "==4.5.2" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "lxml": { + "hashes": [ + "sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21", + "sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0", + "sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297", + "sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591", + "sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801", + "sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f", + "sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541", + "sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58", + "sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6", + "sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730", + "sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49", + "sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc", + "sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879", + "sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6", + "sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e", + "sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c", + "sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2", + "sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d", + "sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485", + "sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e", + "sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73", + "sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488", + "sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5", + "sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11", + "sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298", + "sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "nose": { + "hashes": [ + "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", + "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", + "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + ], + "index": "pypi", + "version": "==1.3.7" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "typed-ast": { + "hashes": [ + "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe", + "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c", + "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2", + "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a", + "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7", + "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827", + "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33", + "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9", + "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032", + "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9", + "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2", + "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2", + "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062", + "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15", + "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357", + "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a", + "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824", + "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442", + "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1", + "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2", + "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.2.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} @@ -1,6 +1,7 @@ # vyos-1x: VyOS 1.2.0+ configuration scripts and data [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield) VyOS 1.1.x had its codebase split into way too many submodules for no good reason, which made it hard to navigate or write meaningful changelogs. As the code undergoes rewrite in the new style in VyOS 1.2.0+, @@ -52,5 +53,11 @@ The guidelines in a nutshell: Tests are executed at build time, you can also execute them by hand with: ``` +pipenv install +pipenv shell make test ``` + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_large)
\ No newline at end of file diff --git a/data/interface-types.json b/data/interface-types.json index c452122af..f5820f403 100644 --- a/data/interface-types.json +++ b/data/interface-types.json @@ -10,6 +10,7 @@ "vti": "vti", "l2tpv3": "l2tpeth", "vxlan": "vxlan", + "wireguard": "wg", "wireless": "wireless", "wirelessmodem": "wlm", "input": "ifb", diff --git a/data/templates/rsyslog/rsyslog.conf b/data/templates/rsyslog/rsyslog.conf new file mode 100644 index 000000000..ab60fc0f0 --- /dev/null +++ b/data/templates/rsyslog/rsyslog.conf @@ -0,0 +1,59 @@ +# /etc/rsyslog.conf Configuration file for rsyslog. +# + +################# +#### MODULES #### +################# + +$ModLoad imuxsock # provides support for local system logging +$ModLoad imklog # provides kernel logging support (previously done by rklogd) +#$ModLoad immark # provides --MARK-- message capability + +$OmitLocalLogging off +$SystemLogSocketName /run/systemd/journal/syslog + +$KLogPath /proc/kmsg + +# provides UDP syslog reception +#$ModLoad imudp +#$UDPServerRun 514 + +# provides TCP syslog reception +#$ModLoad imtcp +#$InputTCPServerRun 514 + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner root +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 + + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + +############### +#### RULES #### +############### +# Emergencies are sent to everybody logged in. + +*.emerg :omusrmsg:* + diff --git a/debian/changelog b/debian/changelog index e8519e87a..00ed8ea6d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,180 @@ +vyos-1x (1.3.0-16) unstable; urgency=low + + * [arp] moving 'show arp' to 'show protocols static arp' + + -- hagbard <vyosdev@derith.de> Thu, 14 Mar 2019 11:15:42 -0700 + +vyos-1x (1.3.0-15) unstable; urgency=low + + * bugfix for show arp interface ethX + + -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 15:28:22 -0700 + +vyos-1x (1.3.0-14) unstable; urgency=low + + * T1294 - Trying to delete 'system syslog' throws an exception/traceback + + -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 15:06:39 -0700 + +vyos-1x (1.3.0-13) unstable; urgency=low + + * T1288 - python implementation of 'set protocols static arp ' + + -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 13:46:19 -0700 + +vyos-1x (1.3.0-12) unstable; urgency=low + + * T1280 - reverted all the changes due to unexpected behaviour + + -- hagbard <vyosdev@derith.de> Fri, 08 Mar 2019 15:18:08 -0800 + +vyos-1x (1.3.0-11) unstable; urgency=low + + * T1282 - make $PreserveFQDN on configurable via cli + + -- hagbard <vyosdev@derith.de> Thu, 07 Mar 2019 13:11:31 -0800 + +vyos-1x (1.3.0-10) unstable; urgency=low + + * T1280 - allow-clients on NTP configuration + + -- hagbard <vyosdev@derith.de> Thu, 07 Mar 2019 12:08:50 -0800 + +vyos-1x (1.3.0-9) unstable; urgency=low + + * mppe cfg options added + + -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 14:52:19 -0800 + +vyos-1x (1.3.0-8) unstable; urgency=low + + * enable option for setting mppe in pptp, default is 128 bit is required. + + -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 14:06:43 -0800 + +vyos-1x (1.3.0-7) unstable; urgency=low + + * op-mode typo fixed + + -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:33:16 -0800 + +vyos-1x (1.3.0-6) unstable; urgency=low + + * version bump for bugfix radius config + + -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:23:25 -0800 + +vyos-1x (1.3.0-5) unstable; urgency=low + + * bumping version for T833: accel-ppp: pptp implementation + + -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:06:45 -0800 + +vyos-1x (1.3.0-4) unstable; urgency=low + + * adding noquery notrust to the default restict option as per T1280 + + -- hagbard <vyosdev@derith.de> Mon, 04 Mar 2019 15:07:17 -0800 + +vyos-1x (1.3.0-3) unstable; urgency=low + + fixes T1262 - dhcp requested WAN ip address doesn't get search parameter in /etc/resolv.conf + + -- hagbard <vyosdev@derith.de> Fri, 22 Feb 2019 10:50:24 -0800 + +vyos-1x (1.3.0-2) unstable; urgency=low + + fixes T1257 - implement 'set system static-host-mapping' in host_name.py and remove old function calls + + -- hagbard <vyosdev@derith.de> Thu, 21 Feb 2019 15:53:30 -0800 + +vyos-1x (1.3.0-1) unstable; urgency=medium + + * Changing version for the new branch. + + -- Daniil Baturin <daniil@baturin.org> Thu, 21 Feb 2019 22:52:26 +0100 + +vyos-1x (1.2.0-14) unstable; urgency=low + + * fixes T1254 - generate wireguard keypair fails when executed on the iso + - keypair can now be generated and used from a running iso + + -- hagbard <vyosdev@derith.de> Tue, 19 Feb 2019 12:31:50 -0800 + +vyos-1x (1.2.0-13) unstable; urgency=low + + * fixes T1238 - Wireguard allows invalid IP's + + -- hagbard <vyosdev@derith.de> Sat, 09 Feb 2019 14:42:13 -0800 + +vyos-1x (1.2.0-12) unstable; urgency=low + + * fixes T1225: wireguard implement 'set int wireguard wg0 peer name disable' to disable single peers + + -- hagbard <vyosdev@derith.de> Mon, 04 Feb 2019 10:26:50 -0800 + +vyos-1x (1.2.0-11) unstable; urgency=low + + * Fix: T1217 - cant delete wireguard wg0 interface + + -- hagbard <vyosdev@derith.de> Wed, 30 Jan 2019 14:54:45 -0800 + +vyos-1x (1.2.0-10) unstable; urgency=low + + * T1178: Scheduled script breaks ability to modify configuration + + -- hagbard <vyosdev@derith.de> Tue, 22 Jan 2019 13:35:09 -0800 + +vyos-1x (1.2.0-9) unstable; urgency=low + + * T1168: Upgrade: 1,1,7 -> 1.2.0-epa2 + - keyword change in config + + -- hagbard <vyosdev@derith.de> Mon, 07 Jan 2019 11:42:49 -0800 + +vyos-1x (1.2.0-8) unstable; urgency=low + + * T1162: WireGuard: Unable to modify tunnels - KeyError: 'state' + + -- hagbard <vyosdev@derith.de> Sun, 06 Jan 2019 15:58:40 -0800 + +vyos-1x (1.2.0-7) unstable; urgency=low + + * T1061: Wireguard: Missing option to administrativly shutdown interface + + -- hagbard <vyosdev@derith.de> Fri, 30 Nov 2018 10:22:41 -0800 + +vyos-1x (1.2.0-6) unstable; urgency=medium + + * adding vyos-accel-ppp-ipoe-kmod for T989 + + -- hagbard <vyosdev@derith.de> Thu, 22 Nov 2018 10:56:15 -0800 + +vyos-1x (1.2.0-5) unstable; urgency=medium + + * T835: accel-ppp: pppoe implementation + + -- hagbard <vyosdev@derith.de> Fri, 09 Nov 2018 10:49:48 -0800 + +vyos-1x (1.2.0-4) unstable; urgency=medium + + * T240 adds feature system integrity check + + -- hagbard <vyosdev@derith.de> Mon, 29 Oct 2018 11:10:18 -0700 + +vyos-1x (1.2.0-3) unstable; urgency=medium + + * T933: adding vmac_xmit_base if use_vmac has been chosen + to avoid split-brain + + -- hagbard <vyosdev@derith.de> Thu, 25 Oct 2018 11:14:44 -0700 + +vyos-1x (1.2.0-2) unstable; urgency=medium + + * T773: adding wireguard support + + -- hagbard <vyosdev@derith.de> Sat, 11 Aug 2018 15:51:34 -0700 + vyos-1x (1.2.0-1) unstable; urgency=medium * T666, T616: new implementation of the VRRP CLI. diff --git a/debian/control b/debian/control index c87e7452c..bf213352e 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,9 @@ Build-Depends: debhelper (>= 9), quilt, python3-lxml, python3-nose, - python3-coverage + python3-coverage, + whois, + libvyosconfig0 (>= 0.0.7) Standards-Version: 3.9.6 Package: vyos-1x @@ -21,21 +23,40 @@ Depends: python3, python3-pystache, python3-psutil, python3-tabulate, + python3-six, + python3-isc-dhcp-leases, + python3-hurry.filesize, + python3-vici (>= 5.7.2), ipaddrcheck, tcpdump, + tshark, bmon, hvinfo, file, lsscsi, pciutils, usbutils, + procps, snmp, snmpd, openssh-server, ntp, + ntpdate, iputils-arping, libvyosconfig0, beep, + isc-dhcp-server, + isc-dhcp-relay, keepalived (>=2.0.5), + wireguard, + tftpd-hpa, + igmpproxy, + vyos-accel-ppp, + vyos-accel-ppp-ipoe-kmod, + mdns-repeater, + udp-broadcast-relay, + pdns-recursor, + lcdproc, + lcdproc-extra-drivers, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/debian/rules b/debian/rules index 15dfec551..ff2d205ba 100755 --- a/debian/rules +++ b/debian/rules @@ -49,7 +49,7 @@ override_dh_auto_install: mkdir -p $(DIR)/$(MIGRATION_SCRIPTS_DIR) cp -r src/migration-scripts/* $(DIR)/$(MIGRATION_SCRIPTS_DIR) - # Install system scripts + # Install system scripts mkdir -p $(DIR)/$(SYSTEM_SCRIPTS_DIR) cp -r src/system/* $(DIR)/$(SYSTEM_SCRIPTS_DIR) @@ -64,3 +64,11 @@ override_dh_auto_install: # Install data files mkdir -p $(DIR)/$(VYOS_DATA_DIR) cp -r data/* $(DIR)/$(VYOS_DATA_DIR) + + # Install etc configuration files + mkdir -p $(DIR)/etc + cp -r src/etc/* $(DIR)/etc + + # Install systemd service units + mkdir -p $(DIR)/lib/systemd/system + cp -r src/systemd/* $(DIR)/lib/systemd/system diff --git a/interface-definitions/arp.xml b/interface-definitions/arp.xml new file mode 100644 index 000000000..b72f025a8 --- /dev/null +++ b/interface-definitions/arp.xml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="static"> + <children> + <tagNode name="arp" owner="${vyos_conf_scripts_dir}/arp.py"> + <properties> + <help>Static ARP translation</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 destination address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="hwaddr"> + <properties> + <help>mac address to translate to</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml index 0437192fa..96ce16639 100644 --- a/interface-definitions/bcast-relay.xml +++ b/interface-definitions/bcast-relay.xml @@ -3,24 +3,36 @@ <interfaceDefinition> <node name="service"> <children> - <node name="broadcast-relay"> + <node name="broadcast-relay" owner="${vyos_conf_scripts_dir}/bcast_relay.py"> <properties> - <help>UDP Broadcast Relay parameters</help> + <help>UDP broadcast relay service</help> + <priority>990</priority> </properties> <children> - <tagNode name="id" owner="${vyos_conf_scripts_dir}/bcast_relay.py"> + <leafNode name="disable"> + <properties> + <help>Globally disable broadcast relay service</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="id"> <properties> <help>Unique ID for each UDP port to forward</help> <valueHelp> <format>1-99</format> <description>Numerical ID #</description> </valueHelp> - <priority>990</priority> <constraint> <validator name="numeric" argument="--range 1-99"/> </constraint> </properties> <children> + <leafNode name="disable"> + <properties> + <help>Disable broadcast relay service instance</help> + <valueless/> + </properties> + </leafNode> <leafNode name="address"> <properties> <help>Set source IP of forwarded packets, otherwise original senders address is used</help> @@ -29,7 +41,7 @@ <description>Optional source address for forwarded packets</description> </valueHelp> <constraint> - <validator name="ipv4"/> + <validator name="ipv4-address"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/dhcp-relay.xml b/interface-definitions/dhcp-relay.xml new file mode 100644 index 000000000..b83402aa1 --- /dev/null +++ b/interface-definitions/dhcp-relay.xml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- DHCP relay configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcp-relay" owner="${vyos_conf_scripts_dir}/dhcp_relay.py"> + <properties> + <help>Host Configuration Protocol (DHCP) relay agent</help> + <priority>910</priority> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>DHCP relay interface [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="relay-options"> + <properties> + <help>Relay options</help> + </properties> + <children> + <leafNode name="hop-count"> + <properties> + <help>Policy to discard packets that have reached specified hop-count</help> + <valueHelp> + <format>1-255</format> + <description>Hop count (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>hop-count must be a value between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="max-size"> + <properties> + <help>Maximum packet size to send to a DHCPv4/BOOTP server</help> + <valueHelp> + <format>64-1400</format> + <description>Maximum packet size (default: 576)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 64-1400"/> + </constraint> + <constraintErrorMessage>max-size must be a value between 64 and 1400</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="relay-agents-packets"> + <properties> + <help>Policy to handle incoming DHCPv4 packets which already contain relay agent options (default: forward)</help> + <completionHelp> + <list>append replace forward discard</list> + </completionHelp> + <valueHelp> + <format>append</format> + <description>append own relay options to packet</description> + </valueHelp> + <valueHelp> + <format>replace</format> + <description>replace existing agent option field</description> + </valueHelp> + <valueHelp> + <format>forward</format> + <description>forward packet unchanged</description> + </valueHelp> + <valueHelp> + <format>discard</format> + <description>discard packet (default action if giaddr not set in packet)</description> + </valueHelp> + <constraint> + <regex>(append|replace|forward|discard)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="server"> + <properties> + <help>DHCP server address</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server IPv4 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcp-server.xml b/interface-definitions/dhcp-server.xml new file mode 100644 index 000000000..87999f496 --- /dev/null +++ b/interface-definitions/dhcp-server.xml @@ -0,0 +1,462 @@ +<?xml version="1.0"?> +<!-- DHCP server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/dhcp_server.py"> + <properties> + <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help> + <priority>911</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dynamic-dns-update"> + <properties> + <help>DHCP server to dynamically update the Domain Name System (DNS)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="global-parameters"> + <properties> + <help>Additional global parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="hostfile-update"> + <properties> + <help>Enable DHCP server updating /etc/hosts (per client lease)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="host-decl-name"> + <properties> + <help>Instruct server to use host declaration name for forward DNS name</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCP shared network name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP pool name</constraintErrorMessage> + </properties> + <children> + <leafNode name="authoritative"> + <properties> + <help>Option to make DHCP server authoritative for this physical network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Shared-network-name description</help> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="shared-network-parameters"> + <properties> + <help>Additional shared-network parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>DHCP subnet for shared network</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="bootfile-name"> + <properties> + <help>Bootstrap file name</help> + </properties> + </leafNode> + <leafNode name="bootfile-server"> + <properties> + <help>Server (IP address or domain name) from which the initial + boot file is to be loaded</help> + </properties> + </leafNode> + <leafNode name="client-prefix-length"> + <properties> + <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> + <valueHelp> + <format>0-32</format> + <description>DHCP client prefix length must be 0 to 32</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-router"> + <properties> + <help>IP address of default router</help> + <valueHelp> + <format>ipv4</format> + <description>Default router IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dns-server"> + <properties> + <help>DNS server IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>DNS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="domain-name"> + <properties> + <help>Client domain name</help> + </properties> + </leafNode> + <leafNode name="domain-search"> + <properties> + <help>Client domain search</help> + <multi/> + </properties> + </leafNode> + <leafNode name="exclude"> + <properties> + <help>IP address that needs to be excluded from DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="failover"> + <properties> + <help>DHCP failover parameters</help> + </properties> + <children> + <leafNode name="local-address"> + <properties> + <help>IP address for failover peer to connect [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name"> + <properties> + <help>DHCP failover peer name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid failover peer name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="peer-address"> + <properties> + <help>IP address of failover peer [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="status"> + <properties> + <help>DHCP failover peer status (primary|secondary) [REQUIRED]</help> + <completionHelp> + <list>primary secondary</list> + </completionHelp> + <constraint> + <regex>(primary|secondary)</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="ip-forwarding"> + <properties> + <help>Enable IP forwarding on client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lease"> + <properties> + <help>Lease timeout in seconds (default: 86400)</help> + <valueHelp> + <format>0-4294967295</format> + <description>DHCP lease time in seconds must be between 0 and 4294967295 (49 days)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>DHCP lease time must be 0 to 4294967295</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="ntp-server"> + <properties> + <help>IP address of NTP server</help> + <valueHelp> + <format>ipv4</format> + <description>NTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="pop-server"> + <properties> + <help>IP address of POP3 server</help> + <valueHelp> + <format>ipv4</format> + <description>POP3 server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="server-identifier"> + <properties> + <help>Address for DHCP server identifier</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server identifier IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="smtp-server"> + <properties> + <help>IP address of SMTP server</help> + <valueHelp> + <format>ipv4</format> + <description>SMTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="range"> + <properties> + <help>DHCP lease range</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP lease range name</constraintErrorMessage> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 start address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 end address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="static-mapping"> + <properties> + <help>Static mapping for specified address type</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid static-mapping name</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static-mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ip-address"> + <properties> + <help>Static mapping for specified IP address [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address used in static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mac-address"> + <properties> + <help>Static mapping for specified MAC address [REQUIRED]</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>MAC address used in static mapping [REQUIRED]</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="static-mapping-parameters"> + <properties> + <help>Additional static-mapping parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="static-route"> + <properties> + <help>Classless static route</help> + </properties> + <children> + <leafNode name="destination-subnet"> + <properties> + <help>Destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="router"> + <properties> + <help>IP address of router to be used to reach the destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of router</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="subnet-parameters"> + <properties> + <help>Additional subnet parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="tftp-server-name"> + <properties> + <help>TFTP server name</help> + </properties> + </leafNode> + <leafNode name="time-offset"> + <properties> + <help>Offset of the client's subnet in seconds from Coordinated Universal Time (UTC)</help> + <constraint> + <regex>^-?[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Invalid time offset valuee</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="time-server"> + <properties> + <help>IP address of time server</help> + <valueHelp> + <format>ipv4</format> + <description>Time server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wins-server"> + <properties> + <help>IP address for Windows Internet Name Service (WINS) server</help> + <valueHelp> + <format>ipv4</format> + <description>WINS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wpad-url"> + <properties> + <help>Web Proxy Autodiscovery (WPAD) URL</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcpv6-relay.xml b/interface-definitions/dhcpv6-relay.xml new file mode 100644 index 000000000..0beb09d05 --- /dev/null +++ b/interface-definitions/dhcpv6-relay.xml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<!-- DHCPv6 relay configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcpv6-relay" owner="${vyos_conf_scripts_dir}/dhcpv6_relay.py"> + <properties> + <help>DHCPv6 Relay Agent parameters</help> + <priority>900</priority> + </properties> + <children> + <tagNode name="listen-interface"> + <properties> + <help>Interface for DHCPv6 Relay Agent to listen for requests</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address on listen-interface listen for requests on</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address on listen interface</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="max-hop-count"> + <properties> + <help>Maximum hop count for which requests will be processed</help> + <valueHelp> + <format>1-255</format> + <description>Hop count (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>max-hop-count must be a value between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="upstream-interface"> + <properties> + <help>Interface for DHCPv6 Relay Agent forward requests out</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address to forward requests to</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of the DHCP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="use-interface-id-option"> + <properties> + <help>Option to set DHCPv6 interface-ID option</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcpv6-server.xml b/interface-definitions/dhcpv6-server.xml new file mode 100644 index 000000000..e63eb2242 --- /dev/null +++ b/interface-definitions/dhcpv6-server.xml @@ -0,0 +1,316 @@ +<?xml version="1.0"?> +<!-- DHCPv6 server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/dhcpv6_server.py"> + <properties> + <help>DHCP for IPv6 (DHCPv6) server</help> + <priority>900</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preference"> + <properties> + <help>Preference of this DHCPv6 server compared with others</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCPv6 shared network name [REQUIRED]</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid DHCPv6 pool name</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>IPv6 DHCP subnet for this shared network [REQUIRED]</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <node name="address-range"> + <properties> + <help>Parameters setting ranges for assigning IPv6 addresses</help> + </properties> + <children> + <tagNode name="prefix"> + <properties> + <help>IPv6 prefix defining range of addresses to assign</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="temporary"> + <properties> + <help>Address range will be used for temporary addresses</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="start"> + <properties> + <help>First in range of consecutive IPv6 addresses to assign</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="stop"> + <properties> + <help>Last in range of consecutive IPv6 addresses</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="domain-search"> + <properties> + <help>Domain name for client to search</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid domain name syntax</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <node name="lease-time"> + <properties> + <help>Parameters relating to the lease time</help> + </properties> + <children> + <leafNode name="default"> + <properties> + <help>Default time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + <leafNode name="maximum"> + <properties> + <help>Maximum time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + <leafNode name="minimum"> + <properties> + <help>Minimum time (in seconds) that will be assigned to a lease</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>IPv6 address of a Recursive DNS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of DNS name server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nis-domain"> + <properties> + <help>NIS domain name for client to use</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid NIS domain name syntax</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nis-server"> + <properties> + <help>IPv6 address of a NIS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nisplus-domain"> + <properties> + <help>NIS+ domain name for client to use</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid NIS+ domain name syntax</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nisplus-server"> + <properties> + <help>IPv6 address of a NIS+ Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS+ server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="prefix-delegation"> + <properties> + <help>Parameters relating to IPv6 prefix delegation</help> + </properties> + <children> + <tagNode name="start"> + <properties> + <help>First in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="prefix-length"> + <properties> + <help>Length in bits of prefixes to be delegated</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="sip-server-address"> + <properties> + <help>IPv6 address of SIP server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of SIP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="sip-server-name"> + <properties> + <help>SIP server name</help> + <constraint> + <regex>^[-_a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid SIP server name syntax</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="sntp-server"> + <properties> + <help>IPv6 address of an SNTP Server for client to use</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="static-mapping"> + <properties> + <help>Name of static mapping</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static-mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="identifier"> + <properties> + <help>Client identifier for this static mapping</help> + </properties> + </leafNode> + <leafNode name="ipv6-address"> + <properties> + <help>Client IPv5 address for this static mapping</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address for this tatic mapping</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dns-domain-name.xml b/interface-definitions/dns-domain-name.xml new file mode 100644 index 000000000..a2c66495f --- /dev/null +++ b/interface-definitions/dns-domain-name.xml @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<!-- host-name configuration --> +<interfaceDefinition> + <node name="system"> + <children> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS)</help> + <priority>400</priority> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>System host name (default: vyos)</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>System domain name</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*</regex> + </constraint> + </properties> + </leafNode> + <node name="domain-search" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Domain Name Server (DNS) domain completion order</help> + <priority>400</priority> + </properties> + <children> + <leafNode name="domain"> + <properties> + <help>DNS domain completion order</help> + <constraint> + <regex>^[-a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid domain name</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-dhcp-nameservers" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Disable DHCP updates of DNS settings</help> + <priority>300</priority> + <valueless/> + </properties> + </leafNode> + <node name="static-host-mapping" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Map host names to addresses</help> + <priority>400</priority> + </properties> + <children> + + <tagNode name="host-name"> + <properties> + <help>Host name for static address mapping</help> + <constraint> + <regex>^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$</regex> + </constraint> + <constraintErrorMessage>invalid hostname</constraintErrorMessage> + </properties> + <children> + <leafNode name="alias"> + <properties> + <help>Alias for this address</help> + <constraint> + <regex>^.{1,63}$</regex> + </constraint> + <constraintErrorMessage>invalid alias hostname, needs to be between 1 and 63 charactes</constraintErrorMessage> + <multi /> + </properties> + </leafNode> + <leafNode name="inet"> + <properties> + <help>IP Address [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 01e8ad7d3..b989434f2 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -35,26 +35,29 @@ <leafNode name="dnssec"> <properties> <help>DNSSEC mode</help> - <valueHelp> - <format>off</format> - <description></description> - </valueHelp> - <valueHelp> - <format>process-no-validate</format> - <description></description> - </valueHelp> - <valueHelp> - <format>process</format> - <description></description> - </valueHelp> - <valueHelp> - <format>log-fail</format> - <description></description> - </valueHelp> - <valueHelp> - <format>validate</format> - <description></description> - </valueHelp> + <completionHelp> + <list>off process-no-validate process log-fail validate</list> + </completionHelp> + <valueHelp> + <format>off</format> + <description>No DNSSEC processing whatsoever!</description> + </valueHelp> + <valueHelp> + <format>process-no-validate</format> + <description>Respond with DNSSEC records to clients that ask for it. Don't do any validation.</description> + </valueHelp> + <valueHelp> + <format>process</format> + <description>Respond with DNSSEC records to clients that ask for it. Validation for clients that request it.</description> + </valueHelp> + <valueHelp> + <format>log-fail</format> + <description>Similar behaviour to process, but validate RRSIGs on responses and log bogus responses.</description> + </valueHelp> + <valueHelp> + <format>validate</format> + <description>Full blown DNSSEC validation. Send SERVFAIL to clients on bogus responses.</description> + </valueHelp> <constraint> <regex>(off|process-no-validate|process|log-fail|validate)</regex> </constraint> @@ -132,7 +135,7 @@ </leafNode> <leafNode name="name-server"> <properties> - <help>Domain Name Servers (DNS) addresses</help> + <help>Domain Name Servers (DNS) addresses [OPTIONAL]</help> <valueHelp> <format>ipv4</format> <description>Domain Name Server (DNS) IPv4 address</description> diff --git a/interface-definitions/dynamic-dns.xml b/interface-definitions/dynamic-dns.xml index e0b2cf764..974bbcbce 100644 --- a/interface-definitions/dynamic-dns.xml +++ b/interface-definitions/dynamic-dns.xml @@ -66,6 +66,9 @@ <tagNode name="service"> <properties> <help>Service being used for Dynamic DNS [REQUIRED]</help> + <completionHelp> + <list>custom afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list> + </completionHelp> <valueHelp> <format>custom</format> <description>Custom or predefined service</description> diff --git a/interface-definitions/host-name.xml b/interface-definitions/host-name.xml deleted file mode 100644 index bbe679607..000000000 --- a/interface-definitions/host-name.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0"?> - -<!-- host-name configuration --> - -<interfaceDefinition> - <node name="system"> - <children> - <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py"> - <properties> - <help>System host name (default: vyos)</help> - <constraint> - <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py"> - <properties> - <help>System domain name</help> - <constraint> - <regex>[A-Za-z0-9][-.A-Za-z0-9]*</regex> - </constraint> - </properties> - </leafNode> - </children> - </node> -</interfaceDefinition> diff --git a/interface-definitions/igmp-proxy.xml b/interface-definitions/igmp-proxy.xml new file mode 100644 index 000000000..ab56019b4 --- /dev/null +++ b/interface-definitions/igmp-proxy.xml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<!-- IGMP Proxy configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="igmp-proxy" owner="${vyos_conf_scripts_dir}/igmp_proxy.py"> + <properties> + <help>Internet Group Management Protocol (IGMP) proxy parameters</help> + <priority>740</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable IGMP proxy</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-quickleave"> + <properties> + <help>Option to disable "quickleave"</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Interface for IGMP proxy [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="alt-subnet"> + <properties> + <help>Allowed unicast sources for multicast traffic to be proxy'ed</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="role"> + <properties> + <help>Role of this IGMP interface</help> + <completionHelp> + <list>upstream downstream disabled</list> + </completionHelp> + <valueHelp> + <format>upstream</format> + <description>Upstream interface (only 1 allowed)</description> + </valueHelp> + <valueHelp> + <format>downstream</format> + <description>Downstream interface(s) (default)</description> + </valueHelp> + <valueHelp> + <format>disabled</format> + <description>Disabled interface</description> + </valueHelp> + <constraint> + <regex>(upstream|downstream|disabled)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="threshold"> + <properties> + <help>TTL threshold</help> + <valueHelp> + <format>1-255</format> + <description>TTL threshold for the interfaces (default: 1)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>threshold must be between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="whitelist"> + <properties> + <help>Group to whitelist</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/mdns-repeater.xml b/interface-definitions/mdns-repeater.xml index d74e203d6..a59321294 100644 --- a/interface-definitions/mdns-repeater.xml +++ b/interface-definitions/mdns-repeater.xml @@ -14,9 +14,15 @@ <priority>990</priority> </properties> <children> + <leafNode name="disable"> + <properties> + <help>Disable mDNS repeater service</help> + <valueless/> + </properties> + </leafNode> <leafNode name="interface"> <properties> - <help>Interface to repeat mdns advertisements to [REQUIRED]</help> + <help>Interface to repeat mDNS advertisements [REQUIRED]</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/ntp.xml b/interface-definitions/ntp.xml index d324404da..945345898 100644 --- a/interface-definitions/ntp.xml +++ b/interface-definitions/ntp.xml @@ -14,12 +14,6 @@ <help>Network Time Protocol (NTP) server</help> </properties> <children> - <leafNode name="dynamic"> - <properties> - <help>Allow server to be configured even if not reachable</help> - <valueless/> - </properties> - </leafNode> <leafNode name="noselect"> <properties> <help>Marks the server as unused</help> diff --git a/interface-definitions/pppoe-server.xml b/interface-definitions/pppoe-server.xml new file mode 100644 index 000000000..993ea262f --- /dev/null +++ b/interface-definitions/pppoe-server.xml @@ -0,0 +1,580 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/accel_pppoe.py"> + <properties> + <help>Point to Point over Ethernet (PPPoE) Server</help> + <priority>900</priority> + </properties> + <children> + <node name="snmp"> + <properties> + <help>Enable SNMP</help> + </properties> + <children> + <leafNode name="master-agent"> + <properties> + <help>enable SNMP master agent mode</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <leafNode name="access-concentrator"> + <properties> + <help>Access concentrator name</help> + <constraint> + <regex>^[a-zA-Z0-9]{1,100}</regex> + </constraint> + <constraintErrorMessage>access-concentrator name limited to aplhanumerical characters only (max. 100)</constraintErrorMessage> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication for remote access PPPoE Server</help> + </properties> + <children> + <node name="local-users"> + <properties> + <help>Local user authentication for PPPoE server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a PPPoE Server user</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <leafNode name="mode"> + <properties> + <help>Authentication mode for PPPoE Server</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use a RADIUS server to autenticate users</description> + </valueHelp> + <constraint> + <regex>^(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> + </leafNode> + <tagNode name="radius-server"> + <properties> + <help>IP address of RADIUS server</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of RADIUS server</description> + </valueHelp> + </properties> + <children> + <leafNode name="secret"> + <properties> + <help>Key for accessing the specified server</help> + </properties> + </leafNode> + <leafNode name="req-limit"> + <properties> + <help>Maximum number of simultaneous requests to server (default: unlimited)</help> + </properties> + </leafNode> + <leafNode name="fail-time"> + <properties> + <help>If server doesn't responds mark it as unavailable for this amount of time in seconds</help> + </properties> + </leafNode> + </children> + </tagNode> + <node name="radius-settings"> + <properties> + <help>RADIUS settings</help> + </properties> + <children> + <leafNode name="timeout"> + <properties> + <help>Timeout to wait response from server (seconds)</help> + </properties> + </leafNode> + <leafNode name="acct-timeout"> + <properties> + <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help> + </properties> + </leafNode> + <leafNode name="max-try"> + <properties> + <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> + </properties> + </leafNode> + <leafNode name="nas-identifier"> + <properties> + <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help> + </properties> + </leafNode> + <leafNode name="nas-ip-address"> + <properties> + <help>Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address.</help> + </properties> + </leafNode> + <node name="dae-server"> + <properties> + <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + <children> + <leafNode name="ip-address"> + <properties> + <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + <leafNode name="secret"> + <properties> + <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + </children> + </node> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="attribute"> + <properties> + <help>Specifies which radius attribute contains rate information. (default is Filter-ID)</help> + </properties> + </leafNode> + <leafNode name="enable"> + <properties> + <help>Enables Bandwidth shaping via RADIUS</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses (must be within a /24)</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Format: ipv6prefix/mask,prefix_len (e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients)</help> + <multi /> + </properties> + </leafNode> + <leafNode name="delegate-prefix"> + <properties> + <help>Format: ipv6prefix/mask,prefix_len (delegate to clients through DHCPv6 prefix delegation - rfc3633)</help> + <multi /> + </properties> + </leafNode> + </children> + </node> + <node name="dns-servers"> + <properties> + <help>IPv4 Domain Name Service (DNS) server</help> + </properties> + <children> + <leafNode name="server-1"> + <properties> + <help>Primary DNS server</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-2"> + <properties> + <help>Secondary DNS server</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="dnsv6-servers"> + <properties> + <help>IPv6 Domain Name Service (DNS) server</help> + </properties> + <children> + <leafNode name="server-1"> + <properties> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <help>Primary DNS server</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-2"> + <properties> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <help>Secondary DNS server</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-3"> + <properties> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <help>Tertiary DNS server</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="interface"> + <properties> + <help>interface(s) to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="local-ip"> + <properties> + <help>local gateway address</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU) - default 1492</help> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + </properties> + </leafNode> + <node name="limits"> + <properties> + <help>Limits the connection rate from a single source</help> + </properties> + <children> + <leafNode name="connection-limit"> + <properties> + <help>Acceptable rate of connections (e.g. 1/min, 60/sec)</help> + <constraint> + <regex>^[0-9]+\/(min|sec)$</regex> + </constraint> + <constraintErrorMessage>illegal value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="burst"> + <properties> + <help>Burst count</help> + </properties> + </leafNode> + <leafNode name="timeout"> + <properties> + <help>Timeout in seconds</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="service-name"> + <properties> + <help>Service name</help> + <constraint> + <regex>^[a-zA-Z0-9\-]{1,100}</regex> + </constraint> + <constraintErrorMessage>servicename can contain aplhanumerical characters and dashes only (max. 100)</constraintErrorMessage> + </properties> + </leafNode> + <node name="wins-servers"> + <properties> + <help>Windows Internet Name Service (WINS) server settings</help> + </properties> + <children> + <leafNode name="server-1"> + <properties> + <help>Primary WINS server</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-2"> + <properties> + <help>Secondary WINS server</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="ppp-options"> + <properties> + <help>Advanced protocol options</help> + </properties> + <children> + <leafNode name="min-mtu"> + <properties> + <help>Minimum acceptable MTU (68-65535)</help> + <constraint> + <validator name="numeric" argument="--range 68-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mru"> + <properties> + <help>Preferred MRU (68-65535)</help> + <constraint> + <validator name="numeric" argument="--range 68-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ccp"> + <properties> + <help>CCP negotiation (default disabled)</help> + <valueless /> + </properties> + </leafNode> + <node name="mppe"> + <properties> + <help>Specifies MPPE negotiation preference. (default prefer mppe)</help> + </properties> + <children> + <leafNode name="require"> + <properties> + <help>Ask client for MPPE, if it rejects then drop the connection</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="prefer"> + <properties> + <help>Ask client for MPPE, if it rejects don't fail</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="deny"> + <properties> + <help>Deny MPPE</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <leafNode name="lcp-echo-interval"> + <properties> + <help>LCP echo-requests/sec</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-failure"> + <properties> + <help>Maximum number of Echo-Requests may be sent without valid reply</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-timeout"> + <properties> + <help>Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used.</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ipv4"> + <properties> + <help>IPv4 (IPCP) negotiation algorithm</help> + <constraint> + <regex>^(deny|allow|prefer|require)</regex> + </constraint> + <constraintErrorMessage>invalid value</constraintErrorMessage> + <valueHelp> + <format>deny</format> + <description>Don't negotiate IPv4</description> + </valueHelp> + <valueHelp> + <format>allow</format> + <description>Negotiate IPv4 only if client requests</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for IPv4 negotiation, don't fail if it rejects</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require IPv4 negotiation</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6"> + <properties> + <help>IPv6 (IPCP6) negotiation algorithm</help> + <constraint> + <regex>^(deny|allow|prefer|require)</regex> + </constraint> + <constraintErrorMessage>invalid value</constraintErrorMessage> + <valueHelp> + <format>deny</format> + <description>Don't negotiate IPv6</description> + </valueHelp> + <valueHelp> + <format>allow</format> + <description>Negotiate IPv6 only if client requests</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for IPv6 negotiation, don't fail if it rejects</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require IPv6 negotiation</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6-intf-id"> + <properties> + <help>Fixed or random interface identifier for IPv6</help> + <valueHelp> + <format>random</format> + <description>Random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>x:x:x:x</format> + <description>specify interface identifier for IPv6</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6-peer-intf-id"> + <properties> + <help>Peer interface identifier for IPv6</help> + <valueHelp> + <format>x:x:x:x</format> + <description>Interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>random</format> + <description>Use a random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description> + </valueHelp> + <valueHelp> + <format>calling-sid</format> + <description>Calculate interface identifier from calling-station-id</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6-accept-peer-intf-id"> + <properties> + <help>Accept peer's interface identifier</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/pptp-server.xml b/interface-definitions/pptp-server.xml new file mode 100644 index 000000000..5d16f8b9f --- /dev/null +++ b/interface-definitions/pptp-server.xml @@ -0,0 +1,254 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="pptp" owner="${vyos_conf_scripts_dir}/accel_pptp.py"> + <properties> + <help>Point to Point Tunneling Protocol (PPTP) Virtual Private Network (VPN)</help> + </properties> + <children> + <node name="remote-access"> + <properties> + <help>Remote access PPTP VPN</help> + </properties> + <children> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="outside-address"> + <properties> + <help>External IP address to which VPN clients will connect</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <node name="dns-servers"> + <properties> + <help>IPv4 Domain Name Service (DNS) server</help> + </properties> + <children> + <leafNode name="server-1"> + <properties> + <help>Primary DNS server</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-2"> + <properties> + <help>Secondary DNS server</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="wins-servers"> + <properties> + <help>Windows Internet Name Service (WINS) server settings</help> + </properties> + <children> + <leafNode name="server-1"> + <properties> + <help>Primary WINS server</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-2"> + <properties> + <help>Secondary WINS server</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses (must be within a /24)</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool (will be used as gateway address)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="gateway-address"> + <properties> + <help>Gatway address uses as client tunnel termination point</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication for remote access PPTP VPN</help> + </properties> + <children> + <leafNode name="require"> + <properties> + <help>Authentication protocol for remote access peer PPTP VPN</help> + <valueHelp> + <format>pap</format> + <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="mppe"> + <properties> + <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help> + <valueHelp> + <format>deny</format> + <description>deny mppe</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>ask client for mppe, if it rejects don't fail</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>ask client for mppe, if it rejects drop connection</description> + </valueHelp> + <constraint> + <regex>^(deny|prefer|require)</regex> + </constraint> + <completionHelp> + <list>deny prefer require</list> + </completionHelp> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>Authentication mode for remote access PPTP VPN</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use a RADIUS server to autenticate users</description> + </valueHelp> + <constraint> + <regex>^(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> + </leafNode> + <node name="local-users"> + <properties> + <help>Local user authentication for remote access PPTP VPN</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a PPTP Server user</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <node name="radius"> + <properties> + <help>RADIUS specific configuration</help> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>IP address of radius server</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of RADIUS server</description> + </valueHelp> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>Key for accessing the specified server</help> + </properties> + </leafNode> + <leafNode name="req-limit"> + <properties> + <help>Maximum number of simultaneous requests to server (default: unlimited)</help> + </properties> + </leafNode> + <leafNode name="fail-time"> + <properties> + <help>If server doesn't responds mark it as unavailable for this amount of time in seconds</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ssh.xml b/interface-definitions/ssh.xml index e8786d202..c0ce976d6 100644 --- a/interface-definitions/ssh.xml +++ b/interface-definitions/ssh.xml @@ -1,7 +1,5 @@ <?xml version="1.0"?> - <!--SSH configuration --> - <interfaceDefinition> <node name="service"> <children> @@ -13,18 +11,23 @@ <children> <node name="access-control"> <properties> - <help>SSH user/group access controls. Directives are processed in this order: deny-users, allow-users, deny-groups and allow-groups</help> + <help>SSH user/group access controls. Directives are processed + in the following order: deny-users, allow-users, deny-groups and + allow-groups.</help> </properties> <children> <node name="allow"> + <properties> + <help>Allow user/group SSH access</help> + </properties> <children> <leafNode name="group"> <properties> <help>Allow members of a group to login</help> - <constraint> - <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> - </constraint> - <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <constraint> + <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> <multi/> </properties> </leafNode> @@ -41,6 +44,9 @@ </children> </node> <node name="deny"> + <properties> + <help>Deny user/group SSH access</help> + </properties> <children> <leafNode name="group"> <properties> @@ -66,12 +72,6 @@ </node> </children> </node> - <leafNode name="allow-root"> - <properties> - <help>Allow the root user to login</help> - <valueless/> - </properties> - </leafNode> <leafNode name="ciphers"> <properties> <help>Allowed ciphers</help> @@ -147,7 +147,7 @@ </leafNode> <leafNode name="mac"> <properties> - <help>Allowed message authentication code (MAC) algorithms</help> + <help>Allowed message authentication code (MAC) algorithms</help> <completionHelp> <script>ssh -Q mac | tr '\n' ' '</script> </completionHelp> @@ -161,11 +161,20 @@ <format>1-65535</format> <description>Numeric IP port</description> </valueHelp> + <multi/> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> </leafNode> + <leafNode name="client-keepalive-interval"> + <properties> + <help>how often send keep alives in seconds</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> </children> </node> </children> diff --git a/interface-definitions/syslog.xml b/interface-definitions/syslog.xml index 0776fff56..3c8d2ebe2 100644 --- a/interface-definitions/syslog.xml +++ b/interface-definitions/syslog.xml @@ -1,5 +1,4 @@ <?xml version="1.0"?> - <interfaceDefinition> <node name="system"> <children> @@ -9,660 +8,683 @@ <priority>400</priority> </properties> <children> - <tagNode name="user"> - <properties> - <help>Logging to specific user's terminal</help> - <constraint> - <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> - </constraint> - <constraintErrorMessage>illegal characters in user</constraintErrorMessage> - <valueHelp> - <format>username</format> - <description>user login name</description> - </valueHelp> - </properties> - <children> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <tagNode name="host"> - <properties> - <help>Logging to a remote host</help> - <constraint> - <!-- at least let's make sure whitespace isn't allowed, ideally it should be checked for IPv4/IPv6 address or fqdn/hostname --> - <regex>[^ ]{1,63}</regex> - </constraint> - <constraintErrorMessage>illegal characters in user</constraintErrorMessage> - <valueHelp> - <format>x.x.x.x or host.domain.tld</format> - <description>Remote host name or IP address</description> - </valueHelp> - </properties> - <children> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="protocol"> - <properties> - <help>syslog communication protocol</help> - <valueHelp> - <format>udp</format> - <description>send log messages to remote syslog server over udp</description> - </valueHelp> - <valueHelp> - <format>tcp</format> - <description>send log messages to remote syslog server over tdp</description> - </valueHelp> - </properties> - </leafNode> - - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <node name="global"> - <children> - <node name="archive"> - <properties> - <help>Log file size and rotation characteristics</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files (default is 5)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files (in kbytes, default is 256)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - </leafNode> - </children> - </node> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency messages</description> - </valueHelp> - <valueHelp> - <format>alert</format> - <description>Urgent messages</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical messages</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error messages</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>Warning messages</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Messages for further investigation</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational messages</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug messages</description> - </valueHelp> - <valueHelp> - <format>all</format> - <description>Log everything</description> - </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> - <tagNode name="file"> - <properties> - <help>Logging to a file</help> - <constraint> - <regex>^[a-zA-Z0-9\-_.]{1,255}</regex> + <tagNode name="user"> + <properties> + <help>Logging to specific user's terminal</help> + <constraint> + <regex>^[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <valueHelp> + <format>username</format> + <description>user login name</description> + </valueHelp> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="host"> + <properties> + <help>Logging to a remote host</help> + <constraint> + <!-- at least let's make sure whitespace isn't allowed, ideally it should be checked for IPv4/IPv6 address or fqdn/hostname --> + <regex>[^ ]{1,63}</regex> + </constraint> + <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <valueHelp> + <format>x.x.x.x or host.domain.tld</format> + <description>Remote host name or IP address</description> + </valueHelp> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="protocol"> + <properties> + <help>syslog communication protocol</help> + <valueHelp> + <format>udp</format> + <description>send log messages to remote syslog server over udp</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>send log messages to remote syslog server over tdp</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="global"> + <properties> + <help>Logging to system standard location</help> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + <node name="marker"> + <properties> + <help>mark messages sent to syslog</help> + </properties> + <children> + <leafNode name="interval"> + <properties> + <help>time interval how often a mark message is being sent in seconds (default: 1200)</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name ="preserve-fqdn"> + <properties> + <help>uses FQDN for logging</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <tagNode name="file"> + <properties> + <help>Logging to a file</help> + <constraint> + <regex>^[a-zA-Z0-9\-_.]{1,255}</regex> </constraint> <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage> - </properties> - <children> - <node name="archive"> - <properties> - <help>Log file size and rotation characteristics</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files (default is 5)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files (in kbytes, default is 256)</help> - <constraint> - <regex>^[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - </leafNode> - </children> - </node> - <tagNode name="facility"> - <properties> - <help>Facility for logging</help> - <completionHelp> - <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All facilities excluding "mark"</description> - </valueHelp> - <valueHelp> - <format>auth</format> - <description>Authentication and authorization</description> - </valueHelp> - <valueHelp> - <format>authpriv</format> - <description>Non-system authorization</description> - </valueHelp> - <valueHelp> - <format>cron</format> - <description>Cron daemon</description> - </valueHelp> - <valueHelp> - <format>daemon</format> - <description>System daemons</description> - </valueHelp> - <valueHelp> - <format>kern</format> - <description>Kernel</description> - </valueHelp> - <valueHelp> - <format>lpr</format> - <description>Line printer spooler</description> - </valueHelp> - <valueHelp> - <format>mail</format> - <description>Mail subsystem</description> - </valueHelp> - <valueHelp> - <format>mark</format> - <description>Timestamp</description> - </valueHelp> - <valueHelp> - <format>news</format> - <description>USENET subsystem</description> - </valueHelp> - <valueHelp> - <format>protocols</format> - <description>depricated will be set to local7</description> - </valueHelp> - <valueHelp> - <format>security</format> - <description>depricated will be set to auth</description> - </valueHelp> - <valueHelp> - <format>syslog</format> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>^[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> <description>Authentication and authorization</description> </valueHelp> - <valueHelp> - <format>user</format> - <description>Application processes</description> - </valueHelp> - <valueHelp> - <format>uucp</format> - <description>UUCP subsystem</description> - </valueHelp> - <valueHelp> - <format>local0</format> - <description>Local facility 0</description> - </valueHelp> - <valueHelp> - <format>local1</format> - <description>Local facility 1</description> - </valueHelp> - <valueHelp> - <format>local2</format> - <description>Local facility 2</description> - </valueHelp> - <valueHelp> - <format>local3</format> - <description>Local facility 3</description> - </valueHelp> - <valueHelp> - <format>local4</format> - <description>Local facility 4</description> - </valueHelp> - <valueHelp> - <format>local5</format> - <description>Local facility 5</description> - </valueHelp> - <valueHelp> - <format>local6</format> - <description>Local facility 6</description> - </valueHelp> - <valueHelp> - <format>local7</format> - <description>Local facility 7</description> - </valueHelp> - </properties> - <children> - <leafNode name="level"> - <properties> - <help>Logging level</help> - <completionHelp> - <list>emerg alert crit err warning notice info debug all</list> - </completionHelp> - <valueHelp> - <format>emerg</format> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <valueHelp> + <format>emerg</format> <description>Emergency messages</description> </valueHelp> <valueHelp> @@ -697,12 +719,12 @@ <format>all</format> <description>Log everything</description> </valueHelp> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> <node name="console"> <properties> <help>logging to serial console</help> @@ -757,7 +779,7 @@ <valueHelp> <format>protocols</format> <description>depricated will be set to local7</description> - </valueHelp> + </valueHelp> <valueHelp> <format>security</format> <description>depricated will be set to auth</description> diff --git a/interface-definitions/tftp-server.xml b/interface-definitions/tftp-server.xml new file mode 100644 index 000000000..2874b034c --- /dev/null +++ b/interface-definitions/tftp-server.xml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<!-- TFTP configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="tftp-server" owner="${vyos_conf_scripts_dir}/tftp_server.py"> + <properties> + <help>Trivial File Transfer Protocol (TFTP) server</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="directory"> + <properties> + <help>Folder containing files served by TFTP [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="allow-upload"> + <properties> + <help>Allow TFTP file uploads</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for TFTP service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 69)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Addresses for TFTP server to listen [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP IPv4 listen address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>TFTP IPv6 listen address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vrrp.xml b/interface-definitions/vrrp.xml index 72419efe9..2884ef613 100644 --- a/interface-definitions/vrrp.xml +++ b/interface-definitions/vrrp.xml @@ -146,7 +146,7 @@ <properties> <help>Preempt delay (in seconds)</help> <constraint> - <validator name="numeric" argument="--positive"/> + <validator name="numeric" argument="--range 0-1000"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/wireguard.xml b/interface-definitions/wireguard.xml new file mode 100644 index 000000000..cc86855a5 --- /dev/null +++ b/interface-definitions/wireguard.xml @@ -0,0 +1,141 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wireguard" owner="${vyos_conf_scripts_dir}/wireguard.py"> + <properties> + <help>WireGuard interface name</help> + <priority>459</priority> <!-- subsequent ones may be removed, just make sure ethernet ifs are present --> + <constraint> + <regex>^wg[0-9]{1,4}</regex> + </constraint> + <constraintErrorMessage>illegal interface name</constraintErrorMessage> + <valueHelp> + <format>wgN</format> + <description>WireGuard interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <constraint> + <validator name="ip-host"/> + </constraint> + <valueHelp> + <format>ipv4-address</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>description</help> + <constraint> + <regex>^.{1,100}$</regex> + </constraint> + <constraintErrorMessage>interface description is too long (limit 100 characters)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>disables interface</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Local port number to accept connections</help> + <constraint> + <validator name="numeric" argument="--range 1024-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>interface mtu size(default: 1420)</help> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fwmark"> + <properties> + <help>A 32-bit fwmark value set on all outgoing packets</help> + <valueHelp> + <format>number</format> + <description>value which marks the packet for QoS/shaper</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <tagNode name="peer"> + <properties> + <help>peer alias</help> + <constraint> + <regex>[^ ]{1,100}$</regex> + </constraint> + <constraintErrorMessage>peer alias too long (limit 100 characters)</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>disables peer</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="pubkey"> + <properties> + <help>base64 encoded public key</help> + <constraint> + <regex>^[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>base64 encoded preshared key</help> + <constraint> + <regex>^[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="allowed-ips"> + <properties> + <help>IP addresses allowed to traverse the peer</help> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <!-- eventually check format IP:port --> + <leafNode name="endpoint"> + <properties> + <help>Remote endpoint (IP:port)</help> + </properties> + </leafNode> + <leafNode name="persistent-keepalive"> + <properties> + <help>how often send keep alives in seconds</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/date.xml b/op-mode-definitions/date.xml new file mode 100644 index 000000000..15a69dbd9 --- /dev/null +++ b/op-mode-definitions/date.xml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="date"> + <properties> + <help>Show system time and date</help> + </properties> + <command>/bin/date</command> + <children> + <node name="utc"> + <properties> + <help>Show system date and time as Coordinated Universal Time</help> + </properties> + <command>/bin/date -u</command> + <children> + <leafNode name="maya"> + <properties> + <help>Show UTC date in Maya calendar format</help> + </properties> + <command>${vyos_op_scripts_dir}/maya_date.py $(date +%s)</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="set"> + <children> + <tagNode name="date"> + <properties> + <help>Set system date and time</help> + <completionHelp> + <list><MMDDhhmm> <MMDDhhmmYY> <MMDDhhmmCCYY> <MMDDhhmmCCYY.ss></list> + </completionHelp> + </properties> + <command>/bin/date "$3"</command> + </tagNode> + <node name="date"> + <properties> + <help>Set system date and time</help> + </properties> + <children> + <node name="ntp"> + <properties> + <help>Set system date and time from NTP server (default: 0.pool.ntp.org)</help> + </properties> + <command>/usr/sbin/ntpdate -u 0.pool.ntp.org</command> + </node> + <tagNode name="ntp"> + <properties> + <help>Set system date and time from NTP server</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ntp_servers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/ntpdate -u "$4"</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml new file mode 100644 index 000000000..a7d09304e --- /dev/null +++ b/op-mode-definitions/dhcp.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="dhcp"> + <properties> + <help>Show DHCP (Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCP information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCP server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP leases for a specific pool</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $4</command> + </tagNode> + </children> + </node> + <node name="statistics"> + <properties> + <help>Show DHCP server statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP server statistics for a specific pool</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $4</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Show DHCPv6 (IPv6 Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCPv6 server information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCPv6 server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dhcp"> + <properties> + <help>Restart DHCP processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo systemctl restart isc-dhcp-server.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv4</command> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Restart DHCPv6 processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCPv6 server process</help> + </properties> + <command>sudo systemctl restart isc-dhcpv6-server.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv6</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml index e789f4aee..ac141174f 100644 --- a/op-mode-definitions/dns-forwarding.xml +++ b/op-mode-definitions/dns-forwarding.xml @@ -1,9 +1,11 @@ <?xml version="1.0"?> - <interfaceDefinition> <node name="show"> <children> <node name="dns"> + <properties> + <help>Show DNS information</help> + </properties> <children> <node name="forwarding"> <properties> @@ -35,7 +37,7 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh</command> </leafNode> - </children> + </children> </node> </children> </node> diff --git a/op-mode-definitions/dynamic-dns.xml b/op-mode-definitions/dynamic-dns.xml index c67769a83..6ea6482e1 100644 --- a/op-mode-definitions/dynamic-dns.xml +++ b/op-mode-definitions/dynamic-dns.xml @@ -1,9 +1,11 @@ <?xml version="1.0"?> - <interfaceDefinition> <node name="show"> <children> <node name="dns"> + <properties> + <help>Show DNS information</help> + </properties> <children> <node name="dynamic"> <properties> @@ -14,7 +16,7 @@ <properties> <help>Show Dynamic DNS status</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dynamic_dns_status.py</command> + <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status</command> </leafNode> </children> </node> @@ -22,4 +24,24 @@ </node> </children> </node> + <node name="update"> + <properties> + <help>Update data for a service</help> + </properties> + <children> + <node name="dns"> + <properties> + <help>Update DNS information</help> + </properties> + <children> + <node name="dynamic"> + <properties> + <help>Update Dynamic DNS information</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command> + </node> + </children> + </node> + </children> + </node> </interfaceDefinition> diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml new file mode 100644 index 000000000..3eadabf0a --- /dev/null +++ b/op-mode-definitions/force-arp.xml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <properties> + <help>Force an operation</help> + </properties> + <children> + <node name="arp"> + <properties> + <help>Send gratuitous ARP request or reply</help> + </properties> + <children> + <node name="reply"> + <properties> + <help>Send gratuitous ARP reply</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP reply on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP reply for specified address</help> + </properties> + <command>sudo arping -I $5 -c 1 -A $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP replies</help> + </properties> + <command>sudo arping -I $5 -c $9 -A $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="request"> + <properties> + <help>Send gratuitous ARP request</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP request on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP request for specified address</help> + </properties> + <command>sudo arping -I $5 -c 1 -U $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP requests</help> + </properties> + <command>sudo arping -I $5 -c $9 -U $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/poweroff.xml b/op-mode-definitions/poweroff.xml index ff27f4f4d..b4163bcb9 100644 --- a/op-mode-definitions/poweroff.xml +++ b/op-mode-definitions/poweroff.xml @@ -4,55 +4,49 @@ <properties> <help>Poweroff the system</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --poweroff now</command> - + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --poweroff</command> <children> <leafNode name="now"> <properties> <help>Poweroff the system without confirmation</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff now</command> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff</command> </leafNode> - <leafNode name="cancel"> <properties> <help>Cancel a pending poweroff</help> </properties> <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command> </leafNode> - - <tagNode name="in"> - <properties> - <help>Poweroff in X minutes</help> - <completionHelp> - <list><Minutes></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $4</command> - - </tagNode> - - <tagNode name="at"> - <properties> - <help>Poweroff at a specific time</help> - <completionHelp> - <list><HH:MM></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3</command> - <children> - <tagNode name="date"> - <properties> - <help>Poweroff at a specific date</help> - <completionHelp> - <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $5</command> - </tagNode> - </children> - </tagNode> - + <tagNode name="in"> + <properties> + <help>Poweroff in X minutes</help> + <completionHelp> + <list><Minutes></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $4</command> + </tagNode> + <tagNode name="at"> + <properties> + <help>Poweroff at a specific time</help> + <completionHelp> + <list><HH:MM></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3</command> + <children> + <tagNode name="date"> + <properties> + <help>Poweroff at a specific date</help> + <completionHelp> + <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $5</command> + </tagNode> + </children> + </tagNode> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml new file mode 100644 index 000000000..d8a518573 --- /dev/null +++ b/op-mode-definitions/pppoe-server.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="pppoe-server"> + <properties> + <help>show pppoe-server status</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active PPPoE server sessions</help> + </properties> + <command>/usr/bin/accel-cmd 'show sessions ifname,username,ip,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes'</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show PPPoE server statistics</help> + </properties> + <command>/usr/bin/accel-cmd 'show stat'</command> + </leafNode> + <leafNode name="interfaces"> + <properties> + <help>Show interfaces where pppoe-server listens on</help> + </properties> + <command>/usr/bin/accel-cmd 'pppoe interface show'</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pptp-server.xml b/op-mode-definitions/pptp-server.xml new file mode 100644 index 000000000..7f141dc53 --- /dev/null +++ b/op-mode-definitions/pptp-server.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="pptp-server"> + <properties> + <help>show pptp-server status</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active PPPoE server sessions</help> + </properties> + <command>/usr/bin/accel-cmd -p 2003 'show sessions'</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reboot.xml b/op-mode-definitions/reboot.xml index 340c8ca82..2c8daec5d 100644 --- a/op-mode-definitions/reboot.xml +++ b/op-mode-definitions/reboot.xml @@ -4,55 +4,49 @@ <properties> <help>Reboot the system</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --reboot now</command> - + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --reboot</command> <children> <leafNode name="now"> <properties> <help>Reboot the system without confirmation</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot now</command> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot</command> </leafNode> - <leafNode name="cancel"> <properties> <help>Cancel a pending reboot</help> </properties> <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command> </leafNode> - - <tagNode name="in"> - <properties> - <help>Reboot in X minutes</help> - <completionHelp> - <list><Minutes></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command> - - </tagNode> - - <tagNode name="at"> - <properties> - <help>Reboot at a specific time</help> - <completionHelp> - <list><HH:MM></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3</command> - <children> - <tagNode name="date"> - <properties> - <help>Reboot at a specific date</help> - <completionHelp> - <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command> - </tagNode> - </children> - </tagNode> - + <tagNode name="in"> + <properties> + <help>Reboot in X minutes</help> + <completionHelp> + <list><Minutes></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command> + </tagNode> + <tagNode name="at"> + <properties> + <help>Reboot at a specific time</help> + <completionHelp> + <list><HH:MM></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3</command> + <children> + <tagNode name="date"> + <properties> + <help>Reboot at a specific date</help> + <completionHelp> + <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command> + </tagNode> + </children> + </tagNode> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-arp.xml b/op-mode-definitions/show-arp.xml index 92c231c6f..1211a7fe5 100644 --- a/op-mode-definitions/show-arp.xml +++ b/op-mode-definitions/show-arp.xml @@ -2,24 +2,31 @@ <interfaceDefinition> <node name="show"> <children> - <node name="arp"> - <properties> - <help>Show Address Resolution Protocol (ARP) information</help> - </properties> - <command>/usr/sbin/arp -e -n</command> - <children> - <tagNode name="interface"> - <properties> - <help>Show Address Resolution Protocol (ARP) cache for specified interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> - </completionHelp> - </properties> - <command>/usr/sbin/arp -e -n -i '$4'</command> - </tagNode> - </children> - </node> - + <node name="protocols"> + <children> + <node name="static"> + <children> + <node name="arp"> + <properties> + <help>Show Address Resolution Protocol (ARP) information</help> + </properties> + <command>/usr/sbin/arp -e -n</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show Address Resolution Protocol (ARP) cache for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + </properties> + <command>/usr/sbin/arp -e -n -i "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-bridge.xml b/op-mode-definitions/show-bridge.xml index a3241951a..8c1f7c398 100644 --- a/op-mode-definitions/show-bridge.xml +++ b/op-mode-definitions/show-bridge.xml @@ -15,23 +15,22 @@ <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script> </completionHelp> </properties> - <command>/sbin/brctl show '$3'</command> + <command>/sbin/brctl show $3</command> <children> <leafNode name="macs"> <properties> <help>Show bridge Media Access Control (MAC) address table</help> </properties> - <command>/sbin/brctl showmacs '$3'</command> + <command>/sbin/brctl showmacs $3</command> </leafNode> <leafNode name="spanning-tree"> <properties> <help>Show bridge spanning tree information</help> </properties> - <command>/sbin/brctl showstp '$3'</command> + <command>/sbin/brctl showstp $3</command> </leafNode> </children> - </tagNode> - + </tagNode> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-configuration.xml b/op-mode-definitions/show-configuration.xml index 90c1533fb..318942ab0 100644 --- a/op-mode-definitions/show-configuration.xml +++ b/op-mode-definitions/show-configuration.xml @@ -2,38 +2,36 @@ <interfaceDefinition> <node name="show"> <children> - <node name="configuration"> - <properties> - <help>Show available saved configurations</help> - </properties> - <!-- no admin check --> - <command>cli-shell-api showCfg --show-active-only --show-hide-secrets</command> - - <children> - <node name="all"> - <properties> - <help>Show running configuration (including default values)</help> - </properties> - <!-- no admin check --> - <command>cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets</command> - </node> - <node name="commands"> - <properties> - <help> Show running configuration as set commands </help> - </properties> - <!-- no admin check --> - <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command> - </node> - <node name="files"> - <properties> - <help> Show available saved configurations </help> - </properties> - <!-- no admin check --> - <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> - </node> - </children> - </node> - + <node name="configuration"> + <properties> + <help>Show available saved configurations</help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-active-only --show-hide-secrets</command> + <children> + <node name="all"> + <properties> + <help>Show running configuration (including default values)</help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets</command> + </node> + <node name="commands"> + <properties> + <help> Show running configuration as set commands </help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command> + </node> + <node name="files"> + <properties> + <help> Show available saved configurations </help> + </properties> + <!-- no admin check --> + <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> + </node> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-date.xml b/op-mode-definitions/show-date.xml deleted file mode 100644 index 705172b39..000000000 --- a/op-mode-definitions/show-date.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="show"> - <children> - <node name="date"> - <properties> - <help>Show system time and date</help> - </properties> - <command>/bin/date</command> - <children> - <node name="utc"> - <properties> - <help>Show system date and time as Coordinated Universal Time</help> - </properties> - <command>/bin/date -u</command> - <children> - <leafNode name="maya"> - <properties> - <help>Show UTC date in Maya calendar format</help> - </properties> - <command>${vyos_op_scripts_dir}/maya_date.py $(date +%s)</command> - </leafNode> - </children> - </node> - </children> - </node> - - </children> - </node> -</interfaceDefinition> diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml index 8a8e35515..37da07fbe 100644 --- a/op-mode-definitions/show-disk.xml +++ b/op-mode-definitions/show-disk.xml @@ -2,23 +2,22 @@ <interfaceDefinition> <node name="show"> <children> - <tagNode name="disk"> - <properties> - <help>Show status of disk device</help> - <completionHelp> - <script>${vyos_completion_dir}/list_disks.sh</script> - </completionHelp> - </properties> - <children> - <leafNode name="format"> - <properties> - <help>Show disk drive formatting</help> - </properties> - <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command> - </leafNode> - </children> - </tagNode> - + <tagNode name="disk"> + <properties> + <help>Show status of disk device</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.sh</script> + </completionHelp> + </properties> + <children> + <leafNode name="format"> + <properties> + <help>Show disk drive formatting</help> + </properties> + <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command> + </leafNode> + </children> + </tagNode> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml index 6cd912aea..a49036397 100644 --- a/op-mode-definitions/show-hardware.xml +++ b/op-mode-definitions/show-hardware.xml @@ -27,21 +27,18 @@ </node> </children> </node> - <node name="dmi"> <properties> <help>Show system DMI details</help> </properties> <command>${vyatta_bindir}/vyatta-show-dmi</command> </node> - <node name="mem"> <properties> <help>Show system RAM details</help> </properties> <command>cat /proc/meminfo</command> </node> - <node name="pci"> <properties> <help>Show system PCI bus details</help> @@ -56,8 +53,6 @@ </node> </children> </node> - - <node name="scsi"> <properties> <help>Show SCSI device information</help> @@ -72,7 +67,6 @@ </node> </children> </node> - <node name="usb"> <properties> <help>Show peripherals connected to the USB bus</help> @@ -87,7 +81,6 @@ </node> </children> </node> - </children> </node> </children> diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml index b3ea129a2..d7f8104aa 100644 --- a/op-mode-definitions/show-host.xml +++ b/op-mode-definitions/show-host.xml @@ -2,36 +2,31 @@ <interfaceDefinition> <node name="show"> <children> - <node name="host"> - <properties> - <help>Show host information</help> - </properties> - <children> - <leafNode name="domain"> - <properties> - <help>Show domain name</help> - </properties> - <command>/bin/domainname -d</command> - </leafNode> - - <leafNode name="name"> - <properties> - <help>Show host name</help> - </properties> - <command>/bin/hostname</command> - </leafNode> - - <tagNode name="lookup"> - <properties> - <help>Lookup host information for hostname|IPv4 address</help> - </properties> - <command>/usr/bin/host $4</command> - </tagNode> - - - </children> - </node> - + <node name="host"> + <properties> + <help>Show host information</help> + </properties> + <children> + <leafNode name="domain"> + <properties> + <help>Show domain name</help> + </properties> + <command>/bin/domainname -d</command> + </leafNode> + <leafNode name="name"> + <properties> + <help>Show host name</help> + </properties> + <command>/bin/hostname</command> + </leafNode> + <tagNode name="lookup"> + <properties> + <help>Lookup host information for hostname|IPv4 address</help> + </properties> + <command>/usr/bin/host $4</command> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-ip-multicast.xml b/op-mode-definitions/show-ip-multicast.xml new file mode 100644 index 000000000..6ffe40436 --- /dev/null +++ b/op-mode-definitions/show-ip-multicast.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>Show IP multicast</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Show multicast interfaces</help> + </properties> + <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --interface; else echo IGMP proxy not configured; fi</command> + </leafNode> + <leafNode name="mfc"> + <properties> + <help>Show multicast fowarding cache</help> + </properties> + <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --mfc; else echo IGMP proxy not configured; fi</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ntp.xml b/op-mode-definitions/show-ntp.xml new file mode 100644 index 000000000..48cee5bee --- /dev/null +++ b/op-mode-definitions/show-ntp.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ntp"> + <properties> + <help>Show peer status of NTP daemon</help> + </properties> + <command>if ps -C ntpd &>/dev/null; then ntpdc -n -c peers; else echo NTP daemon disabled; fi</command> + <children> + <tagNode name="server"> + <properties> + <help>Show date and time of specified NTP server</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ntp_servers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/ntpdate -q "$4"</command> + </tagNode> + <node name="info"> + <properties> + <help>Show NTP operational summary</help> + </properties> + <command>if ps -C ntpd &>/dev/null; then ntpdc -n -c sysinfo; ntpdc -n -c kerninfo; else echo NTP daemon disabled; fi</command> + </node> + + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml index b0930742a..000fd4610 100644 --- a/op-mode-definitions/show-raid.xml +++ b/op-mode-definitions/show-raid.xml @@ -2,17 +2,15 @@ <interfaceDefinition> <node name="show"> <children> - <tagNode name="raid"> - <properties> - <help>Show statis of RAID set</help> - <completionHelp> - <script>${vyos_completion_dir}/list_raidset.sh</script> - </completionHelp> - </properties> - <command>${vyos_op_scripts_dir}/show_raid.sh $3</command> - - </tagNode> - + <tagNode name="raid"> + <properties> + <help>Show statis of RAID set</help> + <completionHelp> + <script>${vyos_completion_dir}/list_raidset.sh</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_raid.sh $3</command> + </tagNode> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/show-systemintegrity.xml b/op-mode-definitions/show-systemintegrity.xml new file mode 100644 index 000000000..44b5faf68 --- /dev/null +++ b/op-mode-definitions/show-systemintegrity.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> + +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name= "system-integrity"> + <properties> + <help>checks the integrity of the system</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml new file mode 100644 index 000000000..db74f867e --- /dev/null +++ b/op-mode-definitions/terminal.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="clear"> + <properties> + <help>Clear system information</help> + </properties> + <children> + <node name="console"> + <properties> + <help>Clear screen</help> + </properties> + <command>/usr/bin/clear</command> + </node> + </children> + </node> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="terminal"> + <properties> + <help>Reset terminal</help> + </properties> + <command>/usr/bin/reset</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml index a6810644e..6d86f7423 100644 --- a/op-mode-definitions/traffic-dump.xml +++ b/op-mode-definitions/traffic-dump.xml @@ -8,7 +8,7 @@ </properties> <children> <tagNode name="interface"> - <command>tcpdump -i $4</command> + <command>sudo tcpdump -i $4</command> <properties> <help>Monitor traffic dump from an interface</help> <completionHelp> @@ -17,19 +17,19 @@ </properties> <children> <tagNode name="filter"> - <command>tcpdump -n -i $4 $6</command> + <command>sudo tcpdump -n -i $4 "${@:6}"</command> <properties> <help>Monitor traffic matching filter conditions</help> </properties> </tagNode> <tagNode name="save"> - <command>tcpdump -n -i $4 -w $6</command> + <command>sudo tcpdump -n -i $4 -w $6</command> <properties> <help>Save traffic dump from an interface to a file</help> </properties> <children> <tagNode name="filter"> - <command>tcpdump -n -i $4 -w $6 $8</command> + <command>sudo tcpdump -n -i $4 -w $6 "${@:8}"</command> <properties> <help>Save a dump of traffic matching filter conditions to a file</help> </properties> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml new file mode 100644 index 000000000..681bb5f47 --- /dev/null +++ b/op-mode-definitions/wireguard.xml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<!-- wireguard key management --> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="wireguard"> + <properties> + <help>wireguard key generation utility</help> + </properties> + <children> + <leafNode name="keypair"> + <properties> + <help>generate a wireguard keypair</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --genkey</command> + </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>generate a wireguard preshared key</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command> + </leafNode> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="wireguard"> + <properties> + <help>Show wireguard properties</help> + </properties> + <children> + <leafNode name="pubkey"> + <properties> + <help>show wireguard public key</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpub</command> + </leafNode> + <leafNode name="privkey"> + <properties> + <help>show wireguard private key</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpriv</command> + </leafNode> + </children> + </node> + <node name="interfaces"> + <children> + <tagNode name="wireguard"> + <properties> + <help>show wireguard interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t wireguard</script> + </completionHelp> + </properties> + <command>sudo wg show "$4"</command> + <children> + <leafNode name="allowed-ips"> + <properties> + <help>show all allowed-ips for the specified interface</help> + </properties> + <command>sudo wg show "$4" allowed-ips</command> + </leafNode> + <leafNode name="endpoints"> + <properties> + <help>show all endpoints for the specified interface</help> + </properties> + <command>sudo wg show "$4" endpoints</command> + </leafNode> + <leafNode name="peers"> + <properties> + <help>show all peer IDs for the specified interface</help> + </properties> + <command>sudo wg show "$4" peers</command> + </leafNode> + <!-- more commands upon request --> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> + diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py new file mode 100644 index 000000000..ec54a1576 --- /dev/null +++ b/python/vyos/component_versions.py @@ -0,0 +1,57 @@ +# Copyright 2017 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +The version data looks like: + +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: +"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1" +=== */ +/* Release version: 1.2.0-rolling+201806131737 */ +""" + +import re + +def get_component_version(string_line): + """ + Get component version dictionary from string + return empty dictionary if string contains no config information + or raise error if component version string malformed + """ + return_value = {} + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line): + + if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line): + raise ValueError("malformed configuration string: " + str(string_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): + if pair[0] in return_value.keys(): + raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"") + return_value[pair[0]] = int(pair[1]) + + return return_value + + +def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'): + """ + Get component version dictionary parsing config file line by line + """ + f = open(config_file_name, 'r') + for line_in_config in f: + component_version = return_version(line_in_config) + if component_version: + return component_version + raise ValueError("no config string in file:", config_file_name) diff --git a/python/vyos/config.py b/python/vyos/config.py index 5af830480..bcf04225b 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -280,8 +280,8 @@ class Config(object): else: try: out = self._run(self._make_command('returnValues', full_path)) - values = out.split() - return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + values = re.findall(r"\'(.*?)\'", out) + return values except VyOSError: return(default) @@ -309,8 +309,8 @@ class Config(object): if self.is_tag(path): try: out = self._run(self._make_command('listNodes', full_path)) - values = out.split() - return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values)) + values = re.findall(r"\'(.*?)\'", out) + return values except VyOSError: return(default) else: diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py new file mode 100644 index 000000000..157011839 --- /dev/null +++ b/python/vyos/configdict.py @@ -0,0 +1,80 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +A library for retrieving value dicts from VyOS configs in a declarative fashion. + +""" + + +def retrieve_config(path_hash, base_path, config): + """ + Retrieves a VyOS config as a dict according to a declarative description + + The description dict, passed in the first argument, must follow this format: + ``field_name : <path, type, [inner_options_dict]>``. + + Supported types are: ``str`` (for normal nodes), + ``list`` (returns a list of strings, for multi nodes), + ``bool`` (returns True if valueless node exists), + ``dict`` (for tag nodes, returns a dict indexed by node names, + according to description in the third item of the tuple). + + Args: + path_hash (dict): Declarative description of the config to retrieve + base_path (list): A base path to prepend to all option paths + config (vyos.config.Config): A VyOS config object + + Returns: + dict: config dict + """ + config_hash = {} + + for k in path_hash: + + if type(path_hash[k]) != tuple: + raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k]))) + if len(path_hash[k]) < 2: + raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k)) + + path = path_hash[k][0] + if type(path) != list: + raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path))) + + typ = path_hash[k][1] + if type(typ) != type: + raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ))) + + path = base_path + path + + path_str = " ".join(path) + + if typ == str: + config_hash[k] = config.return_value(path_str) + elif typ == list: + config_hash[k] = config.return_values(path_str) + elif typ == bool: + config_hash[k] = config.exists(path_str) + elif typ == dict: + try: + inner_hash = path_hash[k][2] + except IndexError: + raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k)) + config_hash[k] = {} + nodes = config.list_nodes(path_str) + for node in nodes: + config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) + + return config_hash diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 4b46a1fb3..a812b62ec 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -24,6 +24,7 @@ def strip_comments(s): IN_COMMENT = 1 i = len(s) - 1 + state = INITIAL config_end = 0 @@ -40,20 +41,19 @@ def strip_comments(s): else: config_end = 0 break - elif (state == INITIAL) and (c == '/'): - # A comment begins, or it's a stray slash - try: - if (s[i-1] == '*'): - state = IN_COMMENT - i -= 2 - else: - raise ValueError("Invalid syntax") - except: - raise ValueError("Invalid syntax") - elif (state == INITIAL) and (c == '}'): - # We are not inside a comment, that's the end of the last node + elif (state == INITIAL) and not re.match(r'(\s|\/)', c): + # Assume there are no (more) trailing comments, + # this is an end of a node: either a brace of the last character + # of a leaf node value config_end = i + 1 break + elif (state == INITIAL) and (c == '/'): + # A comment begins, or it's a stray slash + if (s[i-1] == '*'): + state = IN_COMMENT + i -= 2 + else: + raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1)) elif (state == IN_COMMENT) and (c == '*'): # A comment ends here try: @@ -61,12 +61,13 @@ def strip_comments(s): state = INITIAL i -= 2 except: - raise ValueError("Invalid syntax") + raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1)) elif (state == IN_COMMENT) and (c != '*'): # Ignore everything inside comments, including braces i -= 1 else: - raise ValueError("Invalid syntax") + # Shouldn't happen + raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c)) return (s[0:config_end], s[config_end+1:]) @@ -92,6 +93,10 @@ class ConfigTree(object): self.__from_string.argtypes = [c_char_p] self.__from_string.restype = c_void_p + self.__get_error = self.__lib.get_error + self.__get_error.argtypes = [] + self.__get_error.restype = c_char_p + self.__to_string = self.__lib.to_string self.__to_string.argtypes = [c_void_p] self.__to_string.restype = c_char_p @@ -112,6 +117,14 @@ class ConfigTree(object): self.__delete.argtypes = [c_void_p, c_char_p] self.__delete.restype = c_int + self.__rename = self.__lib.rename_node + self.__rename.argtypes = [c_void_p, c_char_p, c_char_p] + self.__rename.restype = c_int + + self.__copy = self.__lib.copy_node + self.__copy.argtypes = [c_void_p, c_char_p, c_char_p] + self.__copy.restype = c_int + self.__set_replace_value = self.__lib.set_replace_value self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p] self.__set_replace_value.restype = c_int @@ -150,10 +163,12 @@ class ConfigTree(object): config_section, comments_section = strip_comments(config_string) config = self.__from_string(config_section.encode()) if config is None: - raise ValueError("Parse error") + msg = self.__get_error().decode() + raise ValueError("Failed to parse config: {0}".format(msg)) else: self.__config = config self.__comments = comments_section + def __del__(self): if self.__config is not None: self.__destroy(self.__config) @@ -193,6 +208,32 @@ class ConfigTree(object): self.__delete_value(self.__config, path_str, value.encode()) + def rename(self, path, new_name): + check_path(path) + path_str = " ".join(map(str, path)).encode() + newname_str = new_name.encode() + + # Check if a node with intended new name already exists + new_path = path[:-1] + [new_name] + if self.exists(new_path): + raise ConfigTreeError() + res = self.__rename(self.__config, path_str, newname_str) + if (res != 0): + raise ConfigTreeError("Path [{}] doesn't exist".format(oldpath)) + + def copy(self, old_path, new_path): + check_path(old_path) + check_path(new_path) + oldpath_str = " ".join(map(str, old_path)).encode() + newpath_str = " ".join(map(str, new_path)).encode() + + # Check if a node with intended new name already exists + if self.exists(new_path): + raise ConfigTreeError() + res = self.__copy(self.__config, oldpath_str, newpath_str) + if (res != 0): + raise ConfigTreeError("Path [{}] doesn't exist".format(oldpath)) + def exists(self, path): check_path(path) path_str = " ".join(map(str, path)).encode() diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py index 97bb5ae76..e03ccd32b 100644 --- a/python/vyos/limericks.py +++ b/python/vyos/limericks.py @@ -55,6 +55,14 @@ greeted friends with a three-way handshake and refused to proceed if they didn't complete it, that standards-compliant guy Drake. +""", + +""" +A network admin from Nantucket +used hierarchy token buckets. +Bandwidth limits he set +slowed down his net, +users drove him away from Nantucket. """ ] diff --git a/python/vyos/validate.py b/python/vyos/validate.py new file mode 100644 index 000000000..8def0a510 --- /dev/null +++ b/python/vyos/validate.py @@ -0,0 +1,102 @@ +# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import netifaces +import ipaddress + +def is_ipv4(addr): + """ + Check addr if it is an IPv4 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 4: + return True + else: + return False + +def is_ipv6(addr): + """ + Check addr if it is an IPv6 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 6: + return True + else: + return False + +def is_addr_assigned(addr): + """ + Verify if the given IPv4/IPv6 address is assigned to any interface on this + system. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(addr): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type in netifaces.ifaddresses(interface).keys(): + # Check every IP address on this interface for a match + for ip in netifaces.ifaddresses(interface)[addr_type]: + # Check if it matches to the address requested + if ip['addr'] == addr: + return True + + return False + +def is_subnet_connected(subnet, primary=False): + """ + Verify is the given IPv4/IPv6 subnet is connected to any interface on this + system. + + primary check if the subnet is reachable via the primary IP address of this + interface, or in other words has a broadcast address configured. ISC DHCP + for instance will complain if it should listen on non broadcast interfaces. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(subnet): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type not in netifaces.ifaddresses(interface).keys(): + continue + + # An interface can have multiple addresses, but some software components + # only support the primary address :( + if primary: + ip = netifaces.ifaddresses(interface)[addr_type][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet): + return True + else: + # Check every assigned IP address if it is connected to the subnet + # in question + for ip in netifaces.ifaddresses(interface)[addr_type]: + # remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs + addr = ip['addr'].split('%')[0] + if ipaddress.ip_address(addr) in ipaddress.ip_network(subnet): + return True + + return False diff --git a/src/completion/list_ntp_servers.sh b/src/completion/list_ntp_servers.sh new file mode 100755 index 000000000..d0977fbd6 --- /dev/null +++ b/src/completion/list_ntp_servers.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Completion script used to select specific NTP server +/bin/cli-shell-api -- listEffectiveNodes system ntp server | sed "s/'//g" diff --git a/src/conf_mode/accel_pppoe.py b/src/conf_mode/accel_pppoe.py new file mode 100755 index 000000000..e1668d3c9 --- /dev/null +++ b/src/conf_mode/accel_pppoe.py @@ -0,0 +1,587 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import subprocess +import jinja2 +import socket +import time +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +pidfile = r'/var/run/accel_pppoe.pid' +pppoe_cnf_dir = r'/etc/accel-ppp/pppoe' +chap_secrets = pppoe_cnf_dir + '/chap-secrets' +pppoe_conf = pppoe_cnf_dir + '/pppoe.config' +# accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid + +### config path creation +if not os.path.exists(pppoe_cnf_dir): + os.makedirs(pppoe_cnf_dir) + sl.syslog(sl.LOG_NOTICE, pppoe_cnf_dir + " created") + +pppoe_config = ''' +### generated by accel_pppoe.py ### +[modules] +log_syslog +pppoe +ippool +{% if client_ipv6_pool %} +ipv6pool +{% endif %} +chap-secrets +auth_pap +auth_chap_md5 +auth_mschap_v1 +auth_mschap_v2 +#pppd_compat +shaper +{% if snmp == 'enable' or snmp == 'enable-ma' %} +net-snmp +{% endif %} +{% if limits %} +connlimit +{% endif %} +{% if authentication['mode'] == 'radius' %} +radius +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-pppoe,daemon +copy=1 +level=5 + +{% if snmp == 'enable-ma' %} +[snmp] +master=1 +{% endif -%} + +[client-ip-range] +disable + +[ip-pool] +{% if client_ip_pool %} +{{client_ip_pool}} +{% endif %} +gw-ip-address={{ppp_gw}} + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for prfx in client_ipv6_pool['prefix']: %} +{{prfx}} +{% endfor %} +{% for prfx in client_ipv6_pool['delegate-prefix']: %} +delegate={{prfx}} +{% endfor %} +{% endif -%} + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif -%} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif -%} +{% endif -%} + +{% if dnsv6 %} +[dnsv6] +{% for srv in dnsv6: %} +dns={{srv}} +{% endfor %} +{% endif -%} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif -%} +{% endif -%} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/pppoe/chap-secrets +{% endif -%} + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +{% if authentication['radiusopt']['timeout'] %} +timeout={{authentication['radiusopt']['timeout']}} +{% endif %} +{% if authentication['radiusopt']['acct-timeout'] %} +acct-timeout={{authentication['radiusopt']['acct-timeout']}} +{% endif %} +{% if authentication['radiusopt']['max-try'] %} +max-try={{authentication['radiusopt']['max-try']}} +{% endif %} +{% if authentication['radiusopt']['nas-id'] %} +nas-identifier={{authentication['radiusopt']['nas-id']}} +{% endif %} +{% if authentication['radiusopt']['nas-ip'] %} +nas-ip-address={{authentication['radiusopt']['nas-ip']}} +{% endif -%} +{% if authentication['radiusopt']['dae-srv'] %} +dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ +{{authentication['radiusopt']['dae-srv']['port']}},\ +{{authentication['radiusopt']['dae-srv']['secret']}} +{% endif -%} +gw-ip-address={{ppp_gw}} +verbose=1 + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% endif -%} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +single-session=replace +{% if ppp_options['ccp'] %} +ccp=1 +{% endif %} +{% if ppp_options['min-mtu'] %} +min-mtu={{ppp_options['min-mtu']}} +{% else %} +min-mtu={{mtu}} +{% endif %} +{% if ppp_options['mru'] %} +mru={{ppp_options['mru']}} +{% endif %} +{% if ppp_options['mppe'] %} +mppe={{ppp_options['mppe']}} +{% else %} +mppe=prefer +{% endif %} +{% if ppp_options['lcp-echo-interval'] %} +lcp-echo-interval={{ppp_options['lcp-echo-interval']}} +{% else %} +lcp-echo-interval=30 +{% endif %} +{% if ppp_options['lcp-echo-timeout'] %} +lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}} +{% endif %} +{% if ppp_options['lcp-echo-failure'] %} +lcp-echo-failure={{ppp_options['lcp-echo-failure']}} +{% else %} +lcp-echo-failure=3 +{% endif %} +{% if ppp_options['ipv4'] %} +ipv4={{ppp_options['ipv4']}} +{% endif %} +{% if ppp_options['ipv6'] %} +ipv6={{ppp_options['ipv6']}} +{% if ppp_options['ipv6-intf-id'] %} +ipv6-intf-id={{ppp_options['ipv6-intf-id']}} +{% endif %} +{% if ppp_options['ipv6-peer-intf-id'] %} +ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} +{% endif %} +{% if ppp_options['ipv6-accept-peer-intf-id'] %} +ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} +{% endif %} +{% endif %} +mtu={{mtu}} + +[pppoe] +verbose=1 +{% if concentrator %} +ac-name={{concentrator}} +{% endif %} +{% if interface %} +{% for int in interface %} +interface={{int}} +{% endfor %} +{% endif %} +{% if svc_name %} +service-name={{svc_name}} +{% endif %} +pado-delay=0 +# maybe: called-sid, tr101, padi-limit etc. + +{% if limits %} +[connlimit] +limit={{limits['conn-limit']}} +burst={{limits['burst']}} +timeout={{limits['timeout']}} +{% endif %} + +[cli] +tcp=127.0.0.1:2001 +''' + +### pppoe chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses shaper +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ +{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} +{% else %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} +{% endif %} +{% endif %} +{% endfor %} +''' +### +# inline helper functions +### +# depending on hw and threads, daemon needs a little to start +# if it takes longer than 100 * 0.5 secs, exception is being raised +# not sure if that's the best way to check it, but it worked so far quite well +### +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2001)) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start pppoe server") + break + +### chap_secrets file if auth mode local +def write_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd',cmd]).decode().strip() + return ret + except: + return 1 + +### +# inline helper functions end +### + +def get_config(): + c = Config() + if not c.exists('service pppoe-server'): + return None + + config_data = { + 'concentrator' : 'vyos-ac', + 'authentication' : { + 'local-users' : { + }, + 'mode' : 'local', + 'radiussrv' : {}, + 'radiusopt' : {} + }, + 'client_ip_pool' : '', + 'client_ipv6_pool' : {}, + 'interface' : [], + 'ppp_gw' : '', + 'svc_name' : '', + 'dns' : [], + 'dnsv6' : [], + 'wins' : [], + 'mtu' : '1492', + 'ppp_options' : {}, + 'limits' : {}, + 'snmp' : 'disable' + } + + c.set_level('service pppoe-server') + ### general options + if c.exists('access-concentrator'): + config_data['concentrator'] = c.return_value('access-concentrator') + if c.exists('service-name'): + config_data['svc_name'] = c.return_value('service-name') + if c.exists('interface'): + config_data['interface'] = c.return_values('interface') + if c.exists('local-ip'): + config_data['ppp_gw'] = c.return_value('local-ip') + if c.exists('dns-servers'): + if c.return_value('dns-servers server-1'): + config_data['dns'].append(c.return_value('dns-servers server-1')) + if c.return_value('dns-servers server-2'): + config_data['dns'].append(c.return_value('dns-servers server-2')) + if c.exists('dnsv6-servers'): + if c.return_value('dnsv6-servers server-1'): + config_data['dnsv6'].append(c.return_value('dnsv6-servers server-1')) + if c.return_value('dnsv6-servers server-2'): + config_data['dnsv6'].append(c.return_value('dnsv6-servers server-2')) + if c.return_value('dnsv6-servers server-3'): + config_data['dnsv6'].append(c.return_value('dnsv6-servers server-3')) + if c.exists('wins-servers'): + if c.return_value('wins-servers server-1'): + config_data['wins'].append(c.return_value('wins-servers server-1')) + if c.return_value('wins-servers server-2'): + config_data['wins'].append(c.return_value('wins-servers server-2')) + if c.exists('client-ip-pool'): + if c.exists('client-ip-pool start'): + config_data['client_ip_pool'] = c.return_value('client-ip-pool start') + if c.exists('client-ip-pool stop'): + config_data['client_ip_pool'] += '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) + else: + raise ConfigError('client ip pool stop required') + if c.exists('client-ipv6-pool prefix'): + config_data['client_ipv6_pool']['prefix'] = c.return_values('client-ipv6-pool prefix') + if c.exists('client-ipv6-pool delegate-prefix'): + config_data['client_ipv6_pool']['delegate-prefix'] = c.return_values('client-ipv6-pool delegate-prefix') + if c.exists('limits'): + if c.exists('limits burst'): + config_data['limits']['burst'] = str(c.return_value('limits burst')) + if c.exists('limits timeout'): + config_data['limits']['timeout'] = str(c.return_value('limits timeout')) + if c.exists('limits connection-limit'): + config_data['limits']['conn-limit'] = str(c.return_value('limits connection-limit')) + if c.exists('snmp'): + config_data['snmp'] = 'enable' + if c.exists('snmp master-agent'): + config_data['snmp'] = 'enable-ma' + + #### authentication mode local + if not c.exists('authentication mode'): + raise ConfigError('pppoe-server authentication mode required') + + if c.exists('authentication mode local'): + if c.exists('authentication local-users username'): + for usr in c.list_nodes('authentication local-users username'): + config_data['authentication']['local-users'].update( + { + usr : { + 'passwd' : None, + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + } + ) + if c.exists('authentication local-users username ' + usr + ' password'): + config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') + if c.exists('authentication local-users username ' + usr + ' disable'): + config_data['authentication']['local-users'][usr]['state'] = 'disable' + if c.exists('authentication local-users username ' + usr + ' static-ip'): + config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') + if c.exists('authentication local-users username ' + usr + ' rate-limit download'): + config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') + if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): + config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') + + ### authentication mode radius servers and settings + + if c.exists('authentication mode radius'): + config_data['authentication']['mode'] = 'radius' + rsrvs = c.list_nodes('authentication radius-server') + for rsrv in rsrvs: + if c.return_value('authentication radius-server ' + rsrv + ' fail-time') == None: + ftime = '0' + else: + ftime = str(c.return_value('authentication radius-server ' + rsrv + ' fail-time')) + if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: + reql = '0' + else: + reql = str(c.return_value('authentication radius-server ' + rsrv + ' req-limit')) + config_data['authentication']['radiussrv'].update( + { + rsrv : { + 'secret' : c.return_value('authentication radius-server ' + rsrv + ' secret'), + 'fail-time' : ftime, + 'req-limit' : reql + } + } + ) + + #### advanced radius-setting + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings dae-server'): + config_data['authentication']['radiusopt'].update( + { + 'dae-srv' : { + 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), + 'port' : c.return_value('authentication radius-settings dae-server port'), + 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) + } + } + ) + #### filter-id is the internal accel default if attribute is empty + #### set here as default for visibility which may change in the future + if c.exists('authentication radius-settings rate-limit enable'): + if not c.exists('authentication radius-settings rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : 'Filter-ID' + } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : c.return_value('authentication radius-settings rate-limit attribute') + } + + if c.exists('mtu'): + config_data['mtu'] = c.return_value('mtu') + + ### ppp_options + ppp_options = {} + if c.exists('ppp-options'): + if c.exists('ppp-options ccp'): + ppp_options['ccp'] = c.return_value('ppp-options ccp') + if c.exists('ppp-options min-mtu'): + ppp_options['min-mtu'] = c.return_value('ppp-options min-mtu') + if c.exists('ppp-options mru'): + ppp_options['mru'] = c.return_value('ppp-options mru') + if c.exists('ppp-options mppe deny'): + ppp_options['mppe'] = 'deny' + if c.exists('ppp-options mppe require'): + ppp_options['mppe'] = 'requre' + if c.exists('ppp-options mppe prefer'): + ppp_options['mppe'] = 'prefer' + if c.exists('ppp-options lcp-echo-failure'): + ppp_options['lcp-echo-failure'] = c.return_value('ppp-options lcp-echo-failure') + if c.exists('ppp-options lcp-echo-interval'): + ppp_options['lcp-echo-interval'] = c.return_value('ppp-options lcp-echo-interval') + if c.exists('ppp-options ipv4'): + ppp_options['ipv4'] = c.return_value('ppp-options ipv4') + if c.exists('ppp-options ipv6'): + ppp_options['ipv6'] = c.return_value('ppp-options ipv6') + if c.exists('ppp-options ipv6-accept-peer-intf-id'): + ppp_options['ipv6-accept-peer-intf-id']= 1 + if c.exists('ppp-options ipv6-intf-id'): + ppp_options['ipv6-intf-id'] = c.return_value('ppp-options ipv6-intf-id') + if c.exists('ppp-options ipv6-peer-intf-id'): + ppp_options['ipv6-peer-intf-id'] = c.return_value('ppp-options ipv6-peer-intf-id') + if c.exists('ppp-options lcp-echo-timeout'): + ppp_options['lcp-echo-timeout'] = c.return_value('ppp-options lcp-echo-timeout') + + if len(ppp_options) !=0: + config_data['ppp_options'] = ppp_options + + return config_data + +def verify(c): + if c == None: + return None + if c['authentication']['mode'] == 'local': + if not c['authentication']['local-users']: + raise ConfigError('pppoe-server authentication local-users required') + + for usr in c['authentication']['local-users']: + if not c['authentication']['local-users'][usr]['passwd']: + raise ConfigError('user ' + usr + ' requires a password') + ### if up/download is set, check that both have a value + if c['authentication']['local-users'][usr]['upload']: + if not c['authentication']['local-users'][usr]['download']: + raise ConfigError('user ' + usr + ' requires download speed value') + if c['authentication']['local-users'][usr]['download']: + if not c['authentication']['local-users'][usr]['upload']: + raise ConfigError('user ' + usr + ' requires upload speed value') + + if not c['ppp_gw']: + raise ConfigError('pppoe-server local-ip required') + + if c['authentication']['mode'] == 'radius': + if len(c['authentication']['radiussrv']) == 0: + raise ConfigError('radius server required') + for rsrv in c['authentication']['radiussrv']: + if c['authentication']['radiussrv'][rsrv]['secret'] == None: + raise ConfigError('radius server ' + rsrv + ' needs a secret configured') + +def generate(c): + if c == None: + return None + + ### accel-cmd reload doesn't work so any change results in a restart of the daemon + try: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + except KeyError: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + + tmpl = jinja2.Template(pppoe_config, trim_blocks=True) + config_text = tmpl.render(c) + open(pppoe_conf,'w').write(config_text) + + if c['authentication']['local-users']: + write_chap_secrets(c) + + return c + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd','-c',pppoe_conf,'-p',pidfile,'-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/accel_pptp.py b/src/conf_mode/accel_pptp.py new file mode 100755 index 000000000..6c53e8dd4 --- /dev/null +++ b/src/conf_mode/accel_pptp.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import subprocess +import jinja2 +import socket +import time +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +pidfile = r'/var/run/accel_pptp.pid' +pptp_cnf_dir = r'/etc/accel-ppp/pptp' +chap_secrets = pptp_cnf_dir + '/chap-secrets' +pptp_conf = pptp_cnf_dir + '/pptp.config' +# accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid + +### config path creation +if not os.path.exists(pptp_cnf_dir): + os.makedirs(pptp_cnf_dir) + sl.syslog(sl.LOG_NOTICE, pptp_cnf_dir + " created") + +pptp_config = ''' +### generated by accel_pptp.py ### +[modules] +log_syslog +pptp +ippool +chap-secrets +{% if authentication['auth_proto'] %} +{{ authentication['auth_proto'] }} +{% else %} +auth_mschap_v2 +{% endif %} +{% if authentication['mode'] == 'radius' %} +radius +{% endif -%} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-pptp,daemon +copy=1 +level=5 + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif %} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif %} +{% endif %} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif %} +{% endif %} + +[pptp] +bind={{outside_addr}} +verbose=5 +ppp-max-mtu={{mtu}} +mppe={{authentication['mppe']}} +echo-interval=10 +echo-failure=3 + + +[client-ip-range] +0.0.0.0/0 + +[ip-pool] +tunnel={{client_ip_pool}} +gw-ip-address={{gw_ip}} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/pptp/chap-secrets +{% endif %} + +[ppp] +verbose=5 +check-ip=1 +single-session=replace + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +timeout=30 +acct-timeout=30 +max-try=3 +{%endif %} + +[cli] +tcp=127.0.0.1:2003 +''' + +### pptp chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} +{% endif %} +{% endfor %} +''' +### +# inline helper functions +### +# depending on hw and threads, daemon needs a little to start +# if it takes longer than 100 * 0.5 secs, exception is being raised +# not sure if that's the best way to check it, but it worked so far quite well +### +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2003)) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start pptp server") + break + +### chap_secrets file if auth mode local +def write_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2003',cmd]).decode().strip() + return ret + except: + return 1 + +### +# inline helper functions end +### + +def get_config(): + c = Config() + if not c.exists('vpn pptp remote-access '): + return None + + c.set_level('vpn pptp remote-access') + config_data = { + 'authentication' : { + 'mode' : 'local', + 'local-users' : { + }, + 'radiussrv' : {}, + 'auth_proto' : 'auth_mschap_v2', + 'mppe' : 'require' + }, + 'outside_addr' : '', + 'dns' : [], + 'wins' : [], + 'client_ip_pool' : '', + 'mtu' : '1436', + } + + ### general options ### + + if c.exists('dns-servers server-1'): + config_data['dns'].append( c.return_value('dns-servers server-1')) + if c.exists('dns-servers server-2'): + config_data['dns'].append( c.return_value('dns-servers server-2')) + if c.exists('wins-servers server-1'): + config_data['wins'].append( c.return_value('wins-servers server-1')) + if c.exists('wins-servers server-2'): + config_data['wins'].append( c.return_value('wins-servers server-2')) + if c.exists('outside-address'): + config_data['outside_addr'] = c.return_value('outside-address') + + ### auth local + if c.exists('authentication mode local'): + if c.exists('authentication local-users username'): + for usr in c.list_nodes('authentication local-users username'): + config_data['authentication']['local-users'].update( + { + usr : { + 'passwd' : '', + 'state' : 'enabled', + 'ip' : '' + } + } + ) + + if c.exists('authentication local-users username ' + usr + ' password'): + config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') + if c.exists('authentication local-users username ' + usr + ' disable'): + config_data['authentication']['local-users'][usr]['state'] = 'disable' + if c.exists('authentication local-users username ' + usr + ' static-ip'): + config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') + + ### authentication mode radius servers and settings + + if c.exists('authentication mode radius'): + config_data['authentication']['mode'] = 'radius' + rsrvs = c.list_nodes('authentication radius server') + for rsrv in rsrvs: + if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: + ftime = '0' + else: + ftime = str(c.return_value('authentication radius server ' + rsrv + ' fail-time')) + if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: + reql = '0' + else: + reql = str(c.return_value('authentication radius server ' + rsrv + ' req-limit')) + + config_data['authentication']['radiussrv'].update( + { + rsrv : { + 'secret' : c.return_value('authentication radius server ' + rsrv + ' key'), + 'fail-time' : ftime, + 'req-limit' : reql + } + } + ) + + if c.exists('client-ip-pool'): + if c.exists('client-ip-pool start'): + config_data['client_ip_pool'] = c.return_value('client-ip-pool start') + if c.exists('client-ip-pool stop'): + config_data['client_ip_pool'] += '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) + if c.exists('mtu'): + config_data['mtu'] = c.return_value('mtu') + + + ### gateway address + if c.exists('gateway-address'): + config_data['gw_ip'] = c.return_value('gateway-address') + else: + config_data['gw_ip'] = re.sub('[0-9]+$','1',config_data['client_ip_pool']) + + if c.exists('authentication require'): + if c.return_value('authentication require') == 'pap': + config_data['authentication']['auth_proto'] = 'auth_pap' + if c.return_value('authentication require') == 'chap': + config_data['authentication']['auth_proto'] = 'auth_chap_md5' + if c.return_value('authentication require') == 'mschap': + config_data['authentication']['auth_proto'] = 'auth_mschap_v1' + if c.return_value('authentication require') == 'mschap-v2': + config_data['authentication']['auth_proto'] = 'auth_mschap_v2' + + if c.exists('authentication mppe'): + config_data['authentication']['mppe'] = c.return_value('authentication mppe') + + return config_data + +def verify(c): + if c == None: + return None + + if c['authentication']['mode'] == 'local': + if not c['authentication']['local-users']: + raise ConfigError('pppoe-server authentication local-users required') + for usr in c['authentication']['local-users']: + if not c['authentication']['local-users'][usr]['passwd']: + raise ConfigError('user ' + usr + ' requires a password') + + if c['authentication']['mode'] == 'radius': + if len(c['authentication']['radiussrv']) == 0: + raise ConfigError('radius server required') + for rsrv in c['authentication']['radiussrv']: + if c['authentication']['radiussrv'][rsrv]['secret'] == None: + raise ConfigError('radius server ' + rsrv + ' needs a secret configured') + +def generate(c): + if c == None: + return None + + ### accel-cmd reload doesn't work so any change results in a restart of the daemon + try: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + except KeyError: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + + tmpl = jinja2.Template(pptp_config, trim_blocks=True) + config_text = tmpl.render(c) + open(pptp_conf,'w').write(config_text) + + if c['authentication']['local-users']: + write_chap_secrets(c) + + return c + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd','-c',pptp_conf,'-p',pidfile,'-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + ### if gw ip changes, only restart doesn't work + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py new file mode 100755 index 000000000..aeca08432 --- /dev/null +++ b/src/conf_mode/arp.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import syslog as sl +import subprocess + +from vyos.config import Config +from vyos import ConfigError + +arp_cmd = '/usr/sbin/arp' + +def get_config(): + c = Config() + if not c.exists('protocols static arp'): + return None + + c.set_level('protocols static') + config_data = {} + + for ip_addr in c.list_nodes('arp'): + config_data.update( + { + ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr') + } + ) + + return config_data + +def generate(c): + c_eff = Config() + c_eff.set_level('protocols static') + c_eff_cnf = {} + for ip_addr in c_eff.list_effective_nodes('arp'): + c_eff_cnf.update( + { + ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr') + } + ) + + config_data = { + 'remove' : [], + 'update' : {} + } + ### removal + if c == None: + for ip_addr in c_eff_cnf: + config_data['remove'].append(ip_addr) + else: + for ip_addr in c_eff_cnf: + if not ip_addr in c or c[ip_addr] == None: + config_data['remove'].append(ip_addr) + + ### add/update + if c != None: + for ip_addr in c: + if not ip_addr in c_eff_cnf: + config_data['update'][ip_addr] = c[ip_addr] + if ip_addr in c_eff_cnf: + if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None: + config_data['update'][ip_addr] = c[ip_addr] + + return config_data + +def apply(c): + for ip_addr in c['remove']: + sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) + subprocess.call([arp_cmd + ' -d ' + ip_addr + ' >/dev/null 2>&1'], shell=True) + + for ip_addr in c['update']: + sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) + subprocess.call([arp_cmd + ' -s ' + ip_addr + ' ' + c['update'][ip_addr] ], shell=True) + + +if __name__ == '__main__': + try: + c = get_config() + ## syntax verification is done via cli + config = generate(c) + apply(config) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 95f6215b5..8889e701c 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -19,56 +19,105 @@ import sys import os import fnmatch -import subprocess +import jinja2 from vyos.config import Config from vyos import ConfigError config_file = r'/etc/default/udp-broadcast-relay' +config_tmpl = """ +### Autogenerated by bcast_relay.py ### + +# UDP broadcast relay configuration for instance {{ id }} +{%- if description %} +# Comment: {{ description }} +{% endif %} +DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" + +""" + +default_config_data = { + 'disabled': False, + 'instances': [] +} + def get_config(): + relay = default_config_data conf = Config() - conf.set_level("service broadcast-relay id") - relay_id = conf.list_nodes("") - relays = [] - - for id in relay_id: - interface_list = [] - address = conf.return_value("{0} address".format(id)) - description = conf.return_value("{0} description".format(id)) - port = conf.return_value("{0} port".format(id)) - - # split the interface name listing and form a list - if conf.exists("{0} interface".format(id)): - intfs_names = [] - intfs_names = conf.return_values("{0} interface".format(id)) - - for name in intfs_names: - interface_list.append(name) - - relay = { - "id": id, - "address": address, - "description": description, - "interfaces" : interface_list, - "port": port + if not conf.exists('service broadcast-relay'): + return None + else: + conf.set_level('service broadcast-relay') + + # Service can be disabled by user + if conf.exists('disable'): + relay['disabled'] = True + return relay + + # Parse configuration of each individual instance + if conf.exists('id'): + for id in conf.list_nodes('id'): + conf.set_level('service broadcast-relay id {0}'.format(id)) + config = { + 'id': id, + 'disabled': False, + 'address': '', + 'description': '', + 'interfaces': [], + 'port': '' } - relays.append(relay) - return relays + # Check if individual broadcast relay service is disabled + if conf.exists('disable'): + config['disabled'] = True + + # Source IP of forwarded packets, if empty original senders address is used + if conf.exists('address'): + config['address'] = conf.return_value('address') + + # A description for each individual broadcast relay service + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # UDP port to listen on for broadcast frames + if conf.exists('port'): + config['port'] = conf.return_value('port') + + # Network interfaces to listen on for broadcast frames to be relayed + if conf.exists('interface'): + config['interfaces'] = conf.return_values('interface') + + relay['instances'].append(config) -def verify(relays): - for relay in relays: - if not relay["port"]: - raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"])) + return relay - if len(relay["interfaces"]) < 2: - raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"])) +def verify(relay): + if relay is None: + return None + + if relay['disabled']: + return None + + for r in relay['instances']: + # we don't have to check this instance when it's disabled + if r['disabled']: + continue + + # we certainly require a UDP port to listen to + if not r['port']: + raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id'])) + + # Relaying data without two interface is kinda senseless ... + if len(r['interfaces']) < 2: + raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id'])) return None -def generate(relays): - config_header = '### Autogenerated by bcast_relay.py ###\n' + +def generate(relay): + if relay is None: + return None config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) @@ -82,32 +131,43 @@ def generate(relays): # sort our list active_configs.sort() + # delete old configuration files for id in active_configs[:]: - os.unlink(config_file + id) - - for relay in relays: - file = config_file + str(relay["id"]) - interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) - config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) - - f = open(file, 'w') - f.write(config_header) - if relay["description"]: - f.write('# ' + relay["description"] + '\n') - f.write(config_args) - f.close() + if os.path.exists(config_file + id): + os.unlink(config_file + id) + + # If the service is disabled, we can bail out here + if relay['disabled']: + print('Warning: UDP broadcast relay service will be deactivated because it is disabled') + return None + + for r in relay['instances']: + # Skip writing instance config when it's disabled + if r['disabled']: + continue + + # configuration filename contains instance id + file = config_file + str(r['id']) + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(r) + with open(file, 'w') as f: + f.write(config_text) return None -def apply(relays): +def apply(relay): # first stop all running services - cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}" - os.system(cmd) + os.system('sudo systemctl stop udp-broadcast-relay@{1..99}') + + if (relay is None) or relay['disabled']: + return None # start only required service instances - for relay in relays: - cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"]) - os.system(cmd) + for r in relay['instances']: + # Don't start individual instance when it's disabled + if r['disabled']: + continue + os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) return None diff --git a/src/conf_mode/bridge_has_members.py b/src/conf_mode/bridge_has_members.py new file mode 100755 index 000000000..712a9cc46 --- /dev/null +++ b/src/conf_mode/bridge_has_members.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +import vyos.config + +if len(sys.argv) < 2: + print("Argument (bridge interface name) is required") + sys.exit(1) +else: + bridge = sys.argv[1] + +c = vyos.config.Config() + +members = [] + + +# Check in ethernet and bonding interfaces +for p in ["interfaces ethernet", "interfaces bonding"]: + intfs = c.list_nodes(p) + for i in intfs: + intf_bridge_path = "{0} {1} bridge-group bridge".format(p, i) + if c.exists(intf_bridge_path): + intf_bridge = c.return_value(intf_bridge_path) + if intf_bridge == bridge: + members.append(i) + # Walk VLANs + for v in c.list_nodes("{0} {1} vif".format(p, i)): + vif_bridge_path = "{0} {1} vif {2} bridge-group bridge".format(p, i, v) + if c.exists(vif_bridge_path): + vif_bridge = c.return_value(vif_bridge_path) + if vif_bridge == bridge: + members.append("{0}.{1}".format(i, v)) + # Walk QinQ interfaces + for vs in c.list_nodes("{0} {1} vif-s".format(p, i)): + vifs_bridge_path = "{0} {1} vif-s {2} bridge-group bridge".format(p, i, vs) + if c.exists(vifs_bridge_path): + vifs_bridge = c.return_value(vifs_bridge_path) + if vifs_bridge == bridge: + members.append("{0}.{1}".format(i, vs)) + for vc in c.list_nodes("{0} {1} vif-s {2} vif-c".format(p, i, vs)): + vifc_bridge_path = "{0} {1} vif-s {2} vif-c {3} bridge-group bridge".format(p, i, vs, vc) + if c.exists(vifc_bridge_path): + vifc_bridge = c.return_value(vifc_bridge_path) + if vifc_bridge == bridge: + members.append("{0}.{1}.{2}".format(i, vs, vc)) + +# Check tunnel interfaces +for t in c.list_nodes("interfaces tunnel"): + tunnel_bridge_path = "interfaces tunnel {0} parameters ip bridge-group bridge".format(t) + if c.exists(tunnel_bridge_path): + intf_bridge = c.return_value(tunnel_bridge_path) + if intf_bridge == bridge: + members.append(t) + +# Check OpenVPN interfaces +for o in c.list_nodes("interfaces openvpn"): + ovpn_bridge_path = "interfaces openvpn {0} bridge-group bridge".format(o) + if c.exists(ovpn_bridge_path): + intf_bridge = c.return_value(ovpn_bridge_path) + if intf_bridge == bridge: + members.append(o) + +if members: + print("Bridge {0} cannot be deleted because some interfaces are configured as its members".format(bridge)) + print("The following interfaces are members of {0}: {1}".format(bridge, " ".join(members))) + sys.exit(1) +else: + sys.exit(0) diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py new file mode 100755 index 000000000..73e0153df --- /dev/null +++ b/src/conf_mode/dhcp_relay.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/isc-dhcp-relay' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcp_relay.py ### + +# Defaults for isc-dhcp-relay initscript +# sourced by /etc/init.d/isc-dhcp-relay + +# +# This is a POSIX shell fragment +# + +# What servers should the DHCP relay forward requests to? +SERVERS="{{ server | join(' ') }}" + +# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests? +INTERFACES="{{ interface | join(' ') }}" + +# Additional options that are passed to the DHCP relay daemon? +OPTIONS="-4 {{ options | join(' ') }}" +""" + +default_config_data = { + 'interface': [], + 'server': [], + 'options': [], + 'hop_count': '10', + 'relay_agent_packets': 'forward' +} + +def get_config(): + relay = default_config_data + conf = Config() + if not conf.exists('service dhcp-relay'): + return None + else: + conf.set_level('service dhcp-relay') + + # Network interfaces to listen on + if conf.exists('interface'): + relay['interface'] = conf.return_values('interface') + + # Servers equal to the address of the DHCP server(s) + if conf.exists('server'): + relay['server'] = conf.return_values('server') + + conf.set_level('service dhcp-relay relay-options') + + if conf.exists('hop-count'): + count = '-c ' + conf.return_value('hop-count') + relay['options'].append(count) + + # Specify the maximum packet size to send to a DHCPv4/BOOTP server. + # This might be done to allow sufficient space for addition of relay agent + # options while still fitting into the Ethernet MTU size. + # + # Available in DHCPv4 mode only: + if conf.exists('max-size'): + size = '-A ' + conf.return_value('max-size') + relay['options'].append(size) + + # Control the handling of incoming DHCPv4 packets which already contain + # relay agent options. If such a packet does not have giaddr set in its + # header, the DHCP standard requires that the packet be discarded. However, + # if giaddr is set, the relay agent may handle the situation in four ways: + # It may append its own set of relay options to the packet, leaving the + # supplied option field intact; it may replace the existing agent option + # field; it may forward the packet unchanged; or, it may discard it. + # + # Available in DHCPv4 mode only: + if conf.exists('relay-agents-packets'): + pkt = '-a -m ' + conf.return_value('relay-agents-packets') + relay['options'].append(pkt) + + return relay + +def verify(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + if len(relay['interface']) < 2: + # We can only issue a warning otherwise old configurations might break + print('WARNING: At least two interfaces are required for DHCP relay\n' \ + 'to work\n') + + if 'lo' in relay['interface']: + raise ConfigError('DHCP relay does not support the loopback interface.') + + if len(relay['server']) == 0: + raise ConfigError('No DHCP relay server(s) configured.\n' \ + 'At least one DHCP relay server required.') + + return None + +def generate(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(relay) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(relay): + if relay is not None: + os.system('sudo systemctl restart isc-dhcp-relay.service') + else: + # DHCP relay support is removed in the commit + os.system('sudo systemctl stop isc-dhcp-relay.service') + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py new file mode 100755 index 000000000..c8ae2fa91 --- /dev/null +++ b/src/conf_mode/dhcp_server.py @@ -0,0 +1,826 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import ipaddress +import jinja2 +import socket +import struct + +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/dhcp/dhcpd.conf' +lease_file = r'/config/dhcpd.leases' +daemon_config_file = r'/etc/default/isc-dhcp-server' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html +# +# log-facility local7; + +{% if hostfile_update %} +on release { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on expiry { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} +{% endif %} +{%- if host_decl_name %} +use-host-decl-names on; +{%- endif %} +ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; +{% if static_route -%} +option rfc3442-static-route code 121 = array of integer 8; +option windows-static-route code 249 = array of integer 8; +{%- endif %} +{% if wpad -%} +option wpad-url code 252 = text; +{% endif %} + +{%- if global_parameters %} +# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated +{%- for param in global_parameters %} +{{ param }} +{%- endfor -%} +{%- endif %} + +# Failover configuration +{% for network in shared_network %} +{%- if not network.disabled -%} +{%- for subnet in network.subnet %} +{%- if subnet.failover_name -%} +failover peer "{{ subnet.failover_name }}" { +{%- if subnet.failover_status == 'primary' %} + primary; + mclt 1800; + split 128; +{%- elif subnet.failover_status == 'secondary' %} + secondary; +{%- endif %} + address {{ subnet.failover_local_addr }}; + port 520; + peer address {{ subnet.failover_peer_addr }}; + peer port 520; + max-response-delay 30; + max-unacked-updates 10; + load balance max seconds 3; +} +{% endif -%} +{% endfor -%} +{% endif -%} +{% endfor %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- if network.authoritative %} + authoritative; + {%- endif %} + {%- if network.network_parameters %} + # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated + {%- for param in network.network_parameters %} + {{ param }} + {%- endfor %} + {%- endif %} + {%- for subnet in network.subnet %} + subnet {{ subnet.address }} netmask {{ subnet.netmask }} { + {%- if subnet.dns_server %} + option domain-name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.domain_search %} + option domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.ntp_server %} + option ntp-servers {{ subnet.ntp_server | join(', ') }}; + {%- endif %} + {%- if subnet.pop_server %} + option pop-server {{ subnet.pop_server | join(', ') }}; + {%- endif %} + {%- if subnet.smtp_server %} + option smtp-server {{ subnet.smtp_server | join(', ') }}; + {%- endif %} + {%- if subnet.time_server %} + option time-servers {{ subnet.time_server | join(', ') }}; + {%- endif %} + {%- if subnet.wins_server %} + option netbios-name-servers {{ subnet.wins_server | join(', ') }}; + {%- endif %} + {%- if subnet.static_route %} + option rfc3442-static-route {{ subnet.static_route }}; + option windows-static-route {{ subnet.static_route }}; + {%- endif %} + {%- if subnet.ip_forwarding %} + option ip-forwarding true; + {%- endif -%} + {%- if subnet.default_router %} + option routers {{ subnet.default_router }}; + {%- endif -%} + {%- if subnet.server_identifier %} + option dhcp-server-identifier {{ subnet.server_identifier }}; + {%- endif -%} + {%- if subnet.domain_name %} + option domain-name "{{ subnet.domain_name }}"; + {%- endif -%} + {%- if subnet.subnet_parameters %} + # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated + {%- for param in subnet.subnet_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + {%- if subnet.tftp_server %} + option tftp-server-name "{{ subnet.tftp_server }}"; + {%- endif -%} + {%- if subnet.bootfile_name %} + option bootfile-name "{{ subnet.bootfile_name }}"; + filename "{{ subnet.bootfile_name }}"; + {%- endif -%} + {%- if subnet.bootfile_server %} + next-server {{ subnet.bootfile_server }}; + {%- endif -%} + {%- if subnet.time_offset %} + option time-offset {{ subnet.time_offset }}; + {%- endif -%} + {%- if subnet.wpad_url %} + option wpad-url "{{ subnet.wpad_url }}"; + {%- endif -%} + {%- if subnet.client_prefix_length %} + option subnet-mask {{ subnet.client_prefix_length }}; + {%- endif -%} + {% if subnet.lease %} + default-lease-time {{ subnet.lease }}; + max-lease-time {{ subnet.lease }}; + {%- endif -%} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + fixed-address {{ host.ip_address }}; + hardware ethernet {{ host.mac_address }}; + {%- if host.static_parameters %} + # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated + {%- for param in host.static_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + } + {%- endif %} + {%- endfor %} + {%- if subnet.failover_name %} + pool { + failover peer "{{ subnet.failover_name }}"; + deny dynamic bootp clients; + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + } + {%- else %} + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- endif %} + } + {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + {% if hostfile_update -%} + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".", leased-address); + set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); + {%- endif %} + } +} +{%- endif %} +{% endfor %} +""" + +daemon_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcp-server + +DHCPD_CONF=/etc/dhcp/dhcpd.conf +DHCPD_PID=/var/run/dhcpd.pid +OPTIONS="-4 -lf {{ lease_file }}" +INTERFACES="" +""" + +default_config_data = { + 'lease_file': lease_file, + 'disabled': False, + 'ddns_enable': False, + 'global_parameters': [], + 'hostfile_update': False, + 'host_decl_name': False, + 'static_route': False, + 'wpad': False, + 'shared_network': [], +} + +def get_config(): + dhcp = default_config_data + conf = Config() + if not conf.exists('service dhcp-server'): + return None + else: + conf.set_level('service dhcp-server') + + # check for global disable of DHCP service + if conf.exists('disable'): + dhcp['disabled'] = True + + # check for global dynamic DNS upste + if conf.exists('dynamic-dns-update'): + dhcp['ddns_enable'] = True + + # HACKS AND TRICKS + # + # check for global 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters from any user + if conf.exists('global-parameters'): + dhcp['global_parameters'] = conf.return_values('global-parameters') + + # check for global DHCP server updating /etc/host per lease + if conf.exists('hostfile-update'): + dhcp['hostfile_update'] = True + + # If enabled every host declaration within that scope, the name provided + # for the host declaration will be supplied to the client as its hostname. + if conf.exists('host-decl-name'): + dhcp['host_decl_name'] = True + + # check for multiple, shared networks served with DHCP addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcp-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'authoritative': False, + 'description': '', + 'disabled': False, + 'network_parameters': [], + 'subnet': [] + } + # check if DHCP server should be authoritative on this network + if conf.exists('authoritative'): + config['authoritative'] = True + + # A description for this given network + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # If disabled, the shared-network configuration becomes inactive in + # the running DHCP server instance + if conf.exists('disable'): + config['disabled'] = True + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('shared-network-parameters'): + config['network_parameters'] = conf.return_values('shared-network-parameters') + + # check for multiple subnet configurations in a shared network + # config segment + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'address': str(ipaddress.ip_network(net).network_address), + 'netmask': str(ipaddress.ip_network(net).netmask), + 'bootfile_name': '', + 'bootfile_server': '', + 'client_prefix_length': '', + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'exclude': [], + 'failover_local_addr': '', + 'failover_name': '', + 'failover_peer_addr': '', + 'failover_status': '', + 'ip_forwarding': False, + 'lease': '86400', + 'ntp_server': [], + 'pop_server': [], + 'server_identifier': '', + 'smtp_server': [], + 'range': [], + 'static_mapping': [], + 'static_subnet': '', + 'static_router': '', + 'static_route': '', + 'subnet_parameters': [], + 'tftp_server': '', + 'time_offset': '', + 'time_server': [], + 'wins_server': [], + 'wpad_url': '' + } + + # Used to identify a bootstrap file + if conf.exists('bootfile-name'): + subnet['bootfile_name'] = conf.return_value('bootfile-name') + + # Specify host address of the server from which the initial boot file + # (specified above) is to be loaded. Should be a numeric IP address or + # domain name. + if conf.exists('bootfile-server'): + subnet['bootfile_server'] = conf.return_value('bootfile-server') + + # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet + # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet + # mask from the subnet declaration for the network on which an address is being assigned. + if conf.exists('client-prefix-length'): + # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python + host_bits = 32 - int(conf.return_value('client-prefix-length')) + subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) + + # Default router IP address on the client's subnet + if conf.exists('default-router'): + subnet['default_router'] = conf.return_value('default-router') + + # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to + # the client. Servers should be listed in order of preference. + if conf.exists('dns-server'): + subnet['dns_server'] = conf.return_values('dns-server') + + # Option specifies the domain name that client should use when resolving hostnames + # via the Domain Name System. + if conf.exists('domain-name'): + subnet['domain_name'] = conf.return_value('domain-name') + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IP address (local) for failover peer to connect + if conf.exists('failover local-address'): + subnet['failover_local_addr'] = conf.return_value('failover local-address') + + # DHCP failover peer name + if conf.exists('failover name'): + subnet['failover_name'] = conf.return_value('failover name') + + # IP address (remote) of failover peer + if conf.exists('failover peer-address'): + subnet['failover_peer_addr'] = conf.return_value('failover peer-address') + + # DHCP failover peer status (primary|secondary) + if conf.exists('failover status'): + subnet['failover_status'] = conf.return_value('failover status') + + # Option specifies whether the client should configure its IP layer for packet + # forwarding + if conf.exists('ip-forwarding'): + subnet['ip_forwarding'] = True + + # Time should be the length in seconds that will be assigned to a lease if the + # client requesting the lease does not ask for a specific expiration time + if conf.exists('lease'): + subnet['lease'] = conf.return_value('lease') + + # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available + # to the client. + if conf.exists('ntp-server'): + subnet['ntp_server'] = conf.return_values('ntp-server') + + # POP3 server option specifies a list of POP3 servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('pop-server'): + subnet['pop_server'] = conf.return_values('pop-server') + + # DHCP servers include this option in the DHCPOFFER in order to allow the client + # to distinguish between lease offers. DHCP clients use the contents of the + # 'server identifier' field as the destination address for any DHCP messages + # unicast to the DHCP server + if conf.exists('server-identifier'): + subnet['server_identifier'] = conf.return_value('server-identifier') + + # SMTP server option specifies a list of SMTP servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('smtp-server'): + subnet['smtp_server'] = conf.return_values('smtp-server') + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one range statement. The range statement gives the lowest and highest IP + # addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('range'): + for range in conf.list_nodes('range'): + range = { + 'start': conf.return_value('range {0} start'.format(range)), + 'stop': conf.return_value('range {0} stop'.format(range)) + } + subnet['range'].append(range) + + # IP address that needs to be excluded from DHCP lease range + if conf.exists('exclude'): + # We have no need to store the exclude addresses. Exclude addresses + # are recalculated into several ranges + exclude = [] + subnet['exclude'] = conf.return_values('exclude') + for addr in subnet['exclude']: + exclude.append(ipaddress.ip_address(addr)) + + # sort excluded IP addresses ascending + exclude = sorted(exclude) + + # calculate multipe ranges based on the excluded IP addresses + output = [] + for range in subnet['range']: + range_start = range['start'] + range_stop = range['stop'] + + for i in exclude: + # Excluded IP address must be in out specified range + if (i >= ipaddress.ip_address(range_start)) and (i <= ipaddress.ip_address(range_stop)): + # Build up new IP address range ending one IP address before + # our exclude address + range = { + 'start': str(range_start), + 'stop': str(i - 1) + } + # Our next IP address range will start one address after + # our exclude address + range_start = i + 1 + output.append(range) + + # Take care of last IP address range spanning from the last exclude + # address (+1) to the end of the initial configured range + if i is exclude[-1]: + last = { + 'start': str(i + 1), + 'stop': str(range_stop) + } + output.append(last) + else: + # IP address not inside search range, take range is it is + output.append(range) + + # We successfully build up a new list containing several IP address + # ranges, replace IP address range in our dictionary + subnet['range'] = output + + # Static DHCP leases + if conf.exists('static-mapping'): + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disabled': False, + 'ip_address': '', + 'mac_address': '', + 'static_parameters': [] + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disabled'] = True + + # IP address used for this DHCP client + if conf.exists('ip-address'): + mapping['ip_address'] = conf.return_value('ip-address') + + # MAC address of requesting DHCP client + if conf.exists('mac-address'): + mapping['mac_address'] = conf.return_value('mac-address') + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('static-mapping-parameters'): + mapping['static_parameters'] = conf.return_values('static-mapping-parameters') + + # append static-mapping configuration to subnet list + subnet['static_mapping'].append(mapping) + + # Reset config level to matching hirachy + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + + # This option specifies a list of static routes that the client should install in its routing + # cache. If multiple routes to the same destination are specified, they are listed in descending + # order of priority. + if conf.exists('static-route destination-subnet'): + subnet['static_subnet'] = conf.return_value('static-route destination-subnet') + # Required for global config section + dhcp['static_route'] = True + + if conf.exists('static-route router'): + subnet['static_router'] = conf.return_value('static-route router') + + if subnet['static_router'] and subnet['static_subnet']: + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> + # where bytes with the value 0 are omitted. + net = ipaddress.ip_network(subnet['static_subnet']) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + bytes = str(net.network_address).split('.') + for b in bytes: + if b != '0': + string += b + ',' + + # add router bytes + bytes = subnet['static_router'].split('.') + for b in bytes: + string += b + if b is not bytes[-1]: + string += ',' + + subnet['static_route'] = string + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('subnet-parameters'): + subnet['subnet_parameters'] = conf.return_values('subnet-parameters') + + # This option is used to identify a TFTP server and, if supported by the client, should have + # the same effect as the server-name declaration. BOOTP clients are unlikely to support this + # option. Some DHCP clients will support it, and others actually require it. + if conf.exists('tftp-server-name'): + subnet['tftp_server'] = conf.return_value('tftp-server-name') + + # The time-offset option specifies the offset of the client’s subnet in seconds from + # Coordinated Universal Time (UTC). + if conf.exists('time-offset'): + subnet['time_offset'] = conf.return_value('time-offset') + + # The time-server option specifies a list of RFC 868 time servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('time-server'): + subnet['time_server'] = conf.return_values('time-server') + + # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers + # listed in order of preference. NetBIOS Name Service is currently more commonly referred to + # as WINS. WINS servers can be specified using the netbios-name-servers option. + if conf.exists('wins-server'): + subnet['wins_server'] = conf.return_values('wins-server') + + # URL for Web Proxy Autodiscovery Protocol + if conf.exists('wpad-url'): + subnet['wpad_url'] = conf.return_value('wpad-url') + # Required for global config section + dhcp['wpad'] = True + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + # append shared network configuration to config dictionary + dhcp['shared_network'].append(config) + + return dhcp + +def verify(dhcp): + if (dhcp is None) or (dhcp['disabled'] is True): + return None + + # If DHCP is enabled we need one share-network + if len(dhcp['shared_network']) == 0: + raise ConfigError('No DHCP shared networks configured.\n' \ + 'At least one DHCP shared network must be configured.') + + # Inspect shared-network/subnet + failover_names = [] + listen_ok = False + subnets = [] + + # A shared-network requires a subnet definition + for network in dhcp['shared_network']: + if len(network['subnet']) == 0: + raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + for subnet in network['subnet']: + # Subnet static route declaration requires destination and router + if subnet['static_subnet'] or subnet['static_router']: + if not (subnet['static_subnet'] and subnet['static_router']): + raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \ + 'destination-subnet | router') + + # Failover requires all 4 parameters set + if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']: + if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']): + raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \ + 'local-address | peer-address | name | status') + + # Failover names must be uniquie + if subnet['failover_name'] in failover_names: + raise ConfigError('Failover names must be unique:\n' \ + '{0} has already been configured!'.format(subnet['failover_name'])) + else: + failover_names.append(subnet['failover_name']) + + # Failover requires start/stop ranges for pool + if (len(subnet['range']) == 0): + raise ConfigError('At least one start-stop range must be configured for {0}\n' \ + 'to set up DHCP failover!'.format(subnet['network'])) + + # Check if DHCP address range is inside configured subnet declaration + range_start = [] + range_stop = [] + for range in subnet['range']: + start = range['start'] + stop = range['stop'] + # DHCP stop IP required after start IP + if start and not stop: + raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # Range start address must be unique + if start in range_start: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range_start.append(start) + + # Range stop address must be unique + if stop in range_stop: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range_stop.append(stop) + + # Exclude addresses must be in bound + for exclude in subnet['exclude']: + if not ipaddress.ip_address(exclude) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \ + 'under shared network {2}!'.format(exclude, subnet['network'], network['name'])) + + # At least one DHCP address range or static-mapping required + active_mapping = False + if (len(subnet['range']) == 0): + for mapping in subnet['static_mapping']: + # we need at least one active mapping + if (not active_mapping) and (not mapping['disabled']): + active_mapping = True + else: + active_mapping = True + + if not active_mapping: + raise ConfigError('No DHCP address range or active static-mapping set\n' \ + 'for subnet {0}!'.format(subnet['network'])) + + # Static IP address mappings require both an IP address and MAC address + for mapping in subnet['static_mapping']: + # Static IP address must be configured + if not mapping['ip_address']: + raise ConfigError('DHCP static lease IP address not specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) + + # Static IP address must be in bound + if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease subnet {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) + + # Static mapping requires MAC address + if not mapping['mac_address']: + raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) + + # There must be one subnet connected to a listen interface. + # This only counts if the network itself is not disabled! + if not network['disabled']: + if vyos.validate.is_subnet_connected(subnet['network'], primary=True): + listen_ok = True + + # Subnets must be non overlapping + if subnet['network'] in subnets: + raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet)) + else: + subnets.append(subnet['network']) + + # Check for overlapping subnets + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on\n' \ + 'broadcast interfaces. At least one lease subnet must be set such that\n' \ + 'DHCP server listens on a one broadcast interface!') + + return None + +def generate(dhcp): + if dhcp is None: + return None + + if dhcp['disabled'] is True: + print('Warning: DHCP server will be deactivated because it is disabled') + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(dhcp) + + # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters + # we can pass to ISC DHCPd + config_text = config_text.replace(""",'"') + + with open(config_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_tmpl) + config_text = tmpl.render(dhcp) + with open(daemon_config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(dhcp): + if (dhcp is None) or dhcp['disabled']: + # DHCP server is removed in the commit + os.system('sudo systemctl stop isc-dhcp-server.service') + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.exists(daemon_config_file): + os.unlink(daemon_config_file) + else: + # If our file holding DHCP leases does yet not exist - create it + if not os.path.exists(lease_file): + os.mknod(lease_file) + + os.system('sudo systemctl restart isc-dhcp-server.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py new file mode 100755 index 000000000..5868abe8a --- /dev/null +++ b/src/conf_mode/dhcpv6_relay.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/isc-dhcpv6-relay' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcpv6_relay.py ### + +# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay +OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" + +""" + +default_config_data = { + 'listen_addr': [], + 'upstream_addr': [], + 'options': [], +} + +def get_config(): + relay = default_config_data + conf = Config() + if not conf.exists('service dhcpv6-relay'): + return None + else: + conf.set_level('service dhcpv6-relay') + + # Network interfaces/address to listen on for DHCPv6 query(s) + if conf.exists('listen-interface'): + interfaces = conf.list_nodes('listen-interface') + for intf in interfaces: + addr = conf.return_value('listen-interface {0} address'.format(intf)) + listen = addr + '%' + intf + relay['listen_addr'].append(listen) + + # Upstream interface/address for remote DHCPv6 server + if conf.exists('upstream-interface'): + interfaces = conf.list_nodes('upstream-interface') + for intf in interfaces: + addresses = conf.return_values('upstream-interface {0} address'.format(intf)) + for addr in addresses: + server = addr + '%' + intf + relay['upstream_addr'].append(server) + + # Maximum hop count. When forwarding packets, dhcrelay discards packets + # which have reached a hop count of COUNT. Default is 10. Maximum is 255. + if conf.exists('max-hop-count'): + count = '-c ' + conf.return_value('max-hop-count') + relay['options'].append(count) + + if conf.exists('use-interface-id-option'): + relay['options'].append('-I') + + return relay + +def verify(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0: + raise ConfigError('Must set at least one listen and upstream interface.') + + return None + +def generate(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(relay) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(relay): + if relay is not None: + os.system('sudo systemctl restart isc-dhcpv6-relay.service') + else: + # DHCPv6 relay support is removed in the commit + os.system('sudo systemctl stop isc-dhcpv6-relay.service') + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py new file mode 100755 index 000000000..bb3e6e90d --- /dev/null +++ b/src/conf_mode/dhcpv6_server.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import ipaddress + +import jinja2 + +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/dhcp/dhcpd6.conf' +lease_file = r'/config/dhcpd6.leases' +daemon_config_file = r'/etc/default/isc-dhcpv6-server' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcpv6_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html + +log-facility local7; +{%- if preference %} +option dhcp6.preference {{ preference }}; +{%- endif %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- for subnet in network.subnet %} + subnet6 {{ subnet.network }} { + {%- for range in subnet.range6_prefix %} + range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; + {%- endfor %} + {%- for range in subnet.range6 %} + range6 {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- if subnet.domain_search %} + option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.lease_def %} + default-lease-time {{ subnet.lease_def }}; + {%- endif %} + {%- if subnet.lease_max %} + max-lease-time {{ subnet.lease_max }}; + {%- endif %} + {%- if subnet.lease_min %} + min-lease-time {{ subnet.lease_min }}; + {%- endif %} + {%- if subnet.dns_server %} + option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.nis_domain %} + option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; + {%- endif %} + {%- if subnet.nis_server %} + option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; + {%- endif %} + {%- if subnet.nisp_domain %} + option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; + {%- endif %} + {%- if subnet.nisp_server %} + option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; + {%- endif %} + {%- if subnet.sip_address %} + option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; + {%- endif %} + {%- if subnet.sip_hostname %} + option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; + {%- endif %} + {%- if subnet.sntp_server %} + option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; + {%- endif %} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + host-identifier option dhcp6.client-id "{{ host.client_identifier }}"; + fixed-address6 {{ host.ipv6_address }}; + } + {%- endif %} + {%- endfor %} + } + {%- endfor %} +} +{%- endif %} +{% endfor %} + +""" + +daemon_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcpv6-server + +DHCPD_CONF=/etc/dhcp/dhcpd6.conf +DHCPD_PID=/var/run/dhcpd6.pid +OPTIONS="-6 -lf {{ lease_file }}" +INTERFACES="" +""" + +default_config_data = { + 'lease_file': lease_file, + 'preference': '', + 'disabled': False, + 'shared_network': [] +} + +def get_config(): + dhcpv6 = default_config_data + conf = Config() + if not conf.exists('service dhcpv6-server'): + return None + else: + conf.set_level('service dhcpv6-server') + + # Check for global disable of DHCPv6 service + if conf.exists('disable'): + dhcpv6['disabled'] = True + return dhcpv6 + + # Preference of this DHCPv6 server compared with others + if conf.exists('preference'): + dhcpv6['preference'] = conf.return_value('preference') + + # check for multiple, shared networks served with DHCPv6 addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'disabled': False, + 'subnet': [] + } + + # If disabled, the shared-network configuration becomes inactive + if conf.exists('disable'): + config['disabled'] = True + + # check for multiple subnet configurations in a shared network + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'range6_prefix': [], + 'range6': [], + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'lease_def': '', + 'lease_min': '', + 'lease_max': '', + 'nis_domain': '', + 'nis_server': [], + 'nisp_domain': '', + 'nisp_server': [], + 'sip_address': [], + 'sip_hostname': [], + 'sntp_server': [], + 'static_mapping': [] + } + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one address range statement. The range statement gives the lowest and highest + # IP addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('address-range prefix'): + for prefix in conf.list_nodes('address-range prefix'): + range = { + 'prefix': prefix, + 'temporary': False + } + + # Address range will be used for temporary addresses + if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])): + range['temporary'] = True + + # Append to subnet temporary range6 list + subnet['range6_prefix'].append(range) + + if conf.exists('address-range start'): + for range in conf.list_nodes('address-range start'): + range = { + 'start': range, + 'stop': conf.return_value('address-range start {0} stop'.format(range)) + } + + # Append to subnet range6 list + subnet['range6'].append(range) + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IPv6 address valid lifetime + # (at the end the address is no longer usable by the client) + # (set to 30 days, the usual IPv6 default) + if conf.exists('lease-time default'): + subnet['lease_def'] = conf.return_value('lease-time default') + + # Time should be the maximum length in seconds that will be assigned to a lease. + # The only exception to this is that Dynamic BOOTP lease lengths, which are not + # specified by the client, are not limited by this maximum. + if conf.exists('lease-time maximum'): + subnet['lease_max'] = conf.return_value('lease-time maximum') + + # Time should be the minimum length in seconds that will be assigned to a lease + if conf.exists('lease-time minimum'): + subnet['lease_min'] = conf.return_value('lease-time minimum') + + # Specifies a list of Domain Name System name servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('name-server'): + subnet['dns_server'] = conf.return_values('name-server') + + # Ancient NIS (Network Information Service) domain name + if conf.exists('nis-domain'): + subnet['nis_domain'] = conf.return_value('nis-domain') + + # Ancient NIS (Network Information Service) servers + if conf.exists('nis-server'): + subnet['nis_server'] = conf.return_values('nis-server') + + # Ancient NIS+ (Network Information Service) domain name + if conf.exists('nisplus-domain'): + subnet['nisp_domain'] = conf.return_value('nisplus-domain') + + # Ancient NIS+ (Network Information Service) servers + if conf.exists('nisplus-server'): + subnet['nisp_server'] = conf.return_values('nisplus-server') + + # Prefix Delegation (RFC 3633) + if conf.exists('prefix-delegation'): + print('TODO: This option is actually not implemented right now!') + + # Local SIP server that is to be used for all outbound SIP requests - IPv6 address + if conf.exists('sip-server-address'): + subnet['sip_address'] = conf.return_values('sip-server-address') + + # Local SIP server that is to be used for all outbound SIP requests - hostname + if conf.exists('sip-server-name'): + for hostname in conf.return_values('sip-server-name'): + subnet['sip_hostname'].append('"' + hostname + '"') + + # List of local SNTP servers available for the client to synchronize their clocks + if conf.exists('sntp-server'): + subnet['sntp_server'] = conf.return_values('sntp-server') + + # + # Static DHCP v6 leases + # + if conf.exists('static-mapping'): + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disabled': False, + 'ipv6_address': '', + 'client_identifier': '', + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disabled'] = True + + # IPv6 address used for this DHCP client + if conf.exists('ipv6-address'): + mapping['ipv6_address'] = conf.return_value('ipv6-address') + + # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers + if conf.exists('identifier'): + mapping['client_identifier'] = conf.return_value('identifier') + + # append static mapping configuration tu subnet list + subnet['static_mapping'].append(mapping) + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + + # append shared network configuration to config dictionary + dhcpv6['shared_network'].append(config) + + return dhcpv6 + +def verify(dhcpv6): + if dhcpv6 is None: + return None + + if dhcpv6['disabled']: + return None + + # If DHCP is enabled we need one share-network + if len(dhcpv6['shared_network']) == 0: + raise ConfigError('No DHCPv6 shared networks configured.\n' \ + 'At least one DHCPv6 shared network must be configured.') + + # Inspect shared-network/subnet + subnets = [] + listen_ok = False + + for network in dhcpv6['shared_network']: + # A shared-network requires a subnet definition + if len(network['subnet']) == 0: + raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + range6_start = [] + range6_stop = [] + for subnet in network['subnet']: + # Ususal range declaration with a start and stop address + for range6 in subnet['range6']: + # shorten names + start = range6['start'] + stop = range6['stop'] + + # DHCPv6 stop address is required + if start and not stop: + raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # DHCPv6 range start address must be unique - two ranges can't + # start with the same address - makes no sense + if start in range6_start: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range6_start.append(start) + + # DHCPv6 range stop address must be unique - two ranges can't + # end with the same address - makes no sense + if stop in range6_stop: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range6_stop.append(stop) + + # We also have prefixes that require checking + for prefix in subnet['range6_prefix']: + # If configured prefix does not match our subnet, we have to check that it's inside + if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']): + # Configured prefixes must be inside our network + if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + + # DHCPv6 requires at least one configured address range or one static mapping + if not network['disabled']: + if vyos.validate.is_subnet_connected(subnet['network']): + listen_ok = True + + # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping + # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \ + 'this machine. At least one subnet6 must be connected such that\n' \ + 'DHCPv6 listens on an interface!') + + + return None + +def generate(dhcpv6): + if dhcpv6 is None: + return None + + if dhcpv6['disabled']: + print('Warning: DHCPv6 server will be deactivated because it is disabled') + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(dhcpv6) + with open(config_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_tmpl) + config_text = tmpl.render(dhcpv6) + with open(daemon_config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(dhcpv6): + if (dhcpv6 is None) or dhcpv6['disabled']: + # DHCP server is removed in the commit + os.system('sudo systemctl stop isc-dhcpv6-server.service') + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.exists(daemon_config_file): + os.unlink(daemon_config_file) + else: + # If our file holding DHCPv6 leases does yet not exist - create it + if not os.path.exists(lease_file): + os.mknod(lease_file) + + os.system('sudo systemctl restart isc-dhcpv6-server.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 43be9d526..135f6fec0 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -36,9 +36,11 @@ config_tmpl = """ # Non-configurable defaults daemon=yes threads=1 -allow-from=0.0.0.0/0 +allow-from=0.0.0.0/0, ::/0 log-common-errors=yes non-local-bind=yes +query-local-address=0.0.0.0 +query-local-address6=:: # cache-size max-cache-entries={{ cache_size }} @@ -65,8 +67,12 @@ forward-zones={% for d in domains %} # dnssec dnssec={{ dnssec }} +{% if name_servers -%} # name-server forward-zones-recurse=.={{ name_servers | join(';') }} +{% else %} +# no name-servers specified - start full recursor +{% endif %} """ @@ -114,10 +120,10 @@ def get_config(): if conf.exists('domain'): for node in conf.list_nodes('domain'): - server = conf.return_values("domain {0} server".format(node)) + servers = conf.return_values("domain {0} server".format(node)) domain = { "name": node, - "servers": server + "servers": bracketize_ipv6_addrs(servers) } dns['domains'].append(domain) @@ -138,6 +144,8 @@ def get_config(): dns['name_servers'] = dns['name_servers'] + system_name_servers conf.set_level('service dns forwarding') + dns['name_servers'] = bracketize_ipv6_addrs(dns['name_servers']) + if conf.exists('listen-address'): dns['listen_on'] = conf.return_values('listen-address') @@ -193,6 +201,10 @@ def get_config(): return dns +def bracketize_ipv6_addrs(addrs): + """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" + return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] + def verify(dns): # bail out early - looks like removal from running config if dns is None: diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 90d4ff567..ff3c1f825 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -24,6 +24,7 @@ from vyos.config import Config from vyos import ConfigError config_file = r'/etc/ddclient.conf' +cache_file = r'/var/cache/ddclient/ddclient.cache' config_tmpl = """ ### Autogenerated by dynamic_dns.py ### @@ -38,8 +39,8 @@ cache=/var/cache/ddclient/ddclient.cache # # ddclient configuration for interface "{{ interface.interface }}": # -{% if interface.web_url and interface.web_skip -%} -use=web, web={{ interface.web_url}}, web-skip={{ interface.web_skip }} +{% if interface.web_url -%} +use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} {% else -%} use=if, if={{ interface.interface }} {% endif -%} @@ -63,6 +64,9 @@ protocol={{ srv.protocol }} max-interval=28d login={{ srv.login }} password='{{ srv.password }}' +{% if srv.server -%} +server={{ srv.server }} +{% endif -%} {{ host }} {% endfor %} {% endfor %} @@ -148,7 +152,7 @@ def get_config(): 'server': '', 'custom' : False } - + # preload protocol from default service mapping if service in default_service_protocol.keys(): srv['protocol'] = default_service_protocol[service] @@ -242,6 +246,9 @@ def generate(dyndns): return None def apply(dyndns): + if os.path.exists(cache_file): + os.unlink(cache_file) + if dyndns is None: os.system('/etc/init.d/ddclient stop') else: diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 3b3958f7f..81a52e87f 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -24,94 +24,240 @@ import os import re import sys import subprocess +import copy +import jinja2 +import glob from vyos.config import Config from vyos import ConfigError +config_file_hosts = '/etc/hosts' +config_file_resolv = '/etc/resolv.conf' -hosts_file = '/etc/hosts' -hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") -local_addr = '127.0.1.1' # NOSONAR +config_tmpl_hosts = """ +### Autogenerated by host_name.py ### +127.0.0.1 localhost {{ hostname }}{% if domain_name %}.{{ domain_name }}{% endif %} +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +# static hostname mappings +{%- if static_host_mapping['hostnames'] %} +{% for hn in static_host_mapping['hostnames'] -%} +{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}} +{% endfor -%} +{%- endif %} + +### modifications from other scripts should be added below + +""" + +config_tmpl_resolv = """ +### Autogenerated by host_name.py ### +{% for ns in nameserver -%} +nameserver {{ ns }} +{% endfor -%} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{%- if domain_search %} +search {{ domain_search | join(" ") }} +{%- endif %} + +""" + +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolvers = [] + try: + with open(file, 'r') as resolvconf: + for line in resolvconf.readlines(): + line = line.split('#',1)[0]; + line = line.rstrip(); + if 'nameserver' in line: + resolvers.append(line.split()[1]) + return resolvers + except IOError: + return [] + +def get_dhcp_search_doms(file): + search_doms = [] + try: + with open(file, 'r') as resolvconf: + for line in resolvconf.readlines(): + line = line.split('#',1)[0]; + line = line.rstrip(); + if 'search' in line: + return re.sub('^search','',line).lstrip().split() + except IOError: + return [] + +default_config_data = { + 'hostname': 'vyos', + 'domain_name': '', + 'domain_search': [], + 'nameserver': [], + 'no_dhcp_ns': False +} def get_config(): - """Get configuration""" - conf = Config() + conf = Config() + hosts = copy.deepcopy(default_config_data) - hostname = conf.return_value("system host-name") - domain = conf.return_value("system domain-name") + if conf.exists("system host-name"): + hosts['hostname'] = conf.return_value("system host-name") - # No one likes fixups, but we really don't want VyOS fail to boot - # if hostname is not in the config - if not hostname: - hostname = "vyos" + if conf.exists("system domain-name"): + hosts['domain_name'] = conf.return_value("system domain-name") + hosts['domain_search'].append(hosts['domain_name']) - if domain: - fqdn = "{0}.{1}".format(hostname, domain) - else: - fqdn = hostname + for search in conf.return_values("system domain-search domain"): + hosts['domain_search'].append(search) - return {"hostname": hostname, "domain": domain, "fqdn": fqdn} + if conf.exists("system name-server"): + hosts['nameserver'] = conf.return_values("system name-server") + if conf.exists("system disable-dhcp-nameservers"): + hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') -def verify(config): - """Verify configuration""" - # check for invalid host + ## system static-host-mapping + hosts['static_host_mapping'] = { 'hostnames' : {}} - # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" - if not hostname_regex.match(config["hostname"]): - raise ConfigError('Invalid host name ' + config["hostname"]) + if conf.exists('system static-host-mapping host-name'): + for hn in conf.list_nodes('system static-host-mapping host-name'): + hosts['static_host_mapping']['hostnames'][hn] = { + 'ipaddr' : conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), + 'alias' : '' + } + + if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): + a = conf.return_values('system static-host-mapping host-name ' + hn + ' alias') + hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join( conf.return_values('system static-host-mapping host-name ' + hn + ' alias') ) - # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" - length = len(config["hostname"]) - if length < 1 or length > 63: - raise ConfigError( - 'Invalid host-name length, must be less than 63 characters') + return hosts +def verify(config): + if config is None: return None + # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" + hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") + if not hostname_regex.match(config['hostname']): + raise ConfigError('Invalid host name ' + config["hostname"]) -def generate(config): - """Generate configuration files""" - # read the hosts file - with open(hosts_file, 'r') as f: - hosts = f.read() - - # get the current hostname - old_hostname = subprocess.check_output(['hostname']).decode().strip() - - # replace the local host line - vyos_host_line_re = re.compile(r"({}\s+{}.*)".format(local_addr, old_hostname)) - vyos_host_line = "{}\t{} # VyOS entry\n".format(local_addr, config["fqdn"]) - if re.search(vyos_host_line_re, hosts): - hosts = re.sub(vyos_host_line_re, vyos_host_line, hosts) - else: - # On boot (or after errors), the /etc/hosts file has no line for vyos hostname, - # so we have to add it - hosts = "{0}\n{1}".format(hosts, vyos_host_line) - - with open(hosts_file, 'w') as f: - f.write(hosts) + # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" + length = len(config['hostname']) + if length < 1 or length > 63: + raise ConfigError('Invalid host-name length, must be less than 63 characters') + + # The search list is currently limited to six domains with a total of 256 characters. + # https://linux.die.net/man/5/resolv.conf + if len(config['domain_search']) > 6: + raise ConfigError('The search list is currently limited to six domains') + + tmp = ' '.join(config['domain_search']) + if len(tmp) > 256: + raise ConfigError('The search list is currently limited to 256 characters') + # static mappings alias hostname + if config['static_host_mapping']['hostnames']: + for hn in config['static_host_mapping']['hostnames']: + if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: + raise ConfigError('IP address required for ' + hn) + for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): + if not hostname_regex.match(hn_alias) and len (hn_alias) !=0: + raise ConfigError('Invalid hostname alias ' + hn_alias) + + return None + +def generate(config): + if config is None: return None + # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers + # received via dhclient should not be added into the final 'resolv.conf'. + # + # We iterate over every resolver file and retrieve the received nameservers + # for later adjustment of the system nameservers + dhcp_ns = [] + for file in glob.glob('/etc/resolv.conf.dhclient-new*'): + for r in get_resolvers(file): + dhcp_ns.append(r) -def apply(config): - """Apply configuration""" - os.system("hostnamectl set-hostname --static {0}".format(config["fqdn"])) + if not config['no_dhcp_ns']: + config['nameserver'] += dhcp_ns + for file in glob.glob('/etc/resolv.conf.dhclient-new*'): + config['domain_search'] = get_dhcp_search_doms(file) + + # We have third party scripts altering /etc/hosts, too. + # One example are the DHCP hostname update scripts thus we need to cache in + # every modification first - so changing domain-name, domain-search or hostname + # during runtime works + old_hosts = "" + with open(config_file_hosts, 'r') as f: + # Skips text before the beginning of our marker. + # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts + for line in f: + if line.strip() == '### modifications from other scripts should be added below': + break + + for line in f: + # This additional line.strip() filters empty lines + if line.strip(): + old_hosts += line + + # Add an additional newline + old_hosts += '\n' + + tmpl = jinja2.Template(config_tmpl_hosts) + config_text = tmpl.render(config) - # restart services that use the hostname - os.system("systemctl restart rsyslog.service") + with open(config_file_hosts, 'w') as f: + f.write(config_text) + f.write(old_hosts) + tmpl = jinja2.Template(config_tmpl_resolv) + config_text = tmpl.render(config) + with open(config_file_resolv, 'w') as f: + f.write(config_text) + + return None + +def apply(config): + if config is None: return None + fqdn = config['hostname'] + if config['domain_name']: + fqdn += '.' + config['domain_name'] + + os.system("hostnamectl set-hostname --static {0}".format(fqdn.rstrip('.'))) + + # Restart services that use the hostname + os.system("systemctl restart rsyslog.service") + + # If SNMP is running, restart it too + if os.system("pgrep snmpd > /dev/null") == 0: + os.system("systemctl restart snmpd.service") + + # restart pdns if it is used + if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: + os.system("/etc/init.d/pdns-recursor restart >/dev/null") + + return None if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py new file mode 100755 index 000000000..b994369af --- /dev/null +++ b/src/conf_mode/igmp_proxy.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/igmpproxy.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +######################################################## +# +# autogenerated by igmp_proxy.py +# +# The configuration file must define one upstream +# interface, and one or more downstream interfaces. +# +# If multicast traffic originates outside the +# upstream subnet, the "altnet" option can be +# used in order to define legal multicast sources. +# (Se example...) +# +# The "quickleave" should be used to avoid saturation +# of the upstream link. The option should only +# be used if it's absolutely nessecary to +# accurately imitate just one Client. +# +######################################################## + +{% if not disable_quickleave -%} +quickleave +{% endif -%} + +{% for i in interface %} +# Configuration for {{ i.interface }} ({{ i.role }} interface) +{% if i.role == 'disabled' -%} +phyint {{ i.interface }} disabled +{%- else -%} +phyint {{ i.interface }} {{ i.role }} ratelimit 0 threshold {{ i.threshold }} +{%- endif -%} +{%- for subnet in i.alt_subnet %} + altnet {{ subnet }} +{%- endfor %} +{%- for subnet in i.whitelist %} + whitelist {{ subnet }} +{%- endfor %} +{% endfor %} +""" + +default_config_data = { + 'disable': False, + 'disable_quickleave': False, + 'interface': [], +} + +def get_config(): + igmp_proxy = default_config_data + conf = Config() + if not conf.exists('protocols igmp-proxy'): + return None + else: + conf.set_level('protocols igmp-proxy') + + # Network interfaces to listen on + if conf.exists('disable'): + igmp_proxy['disable'] = True + + # Option to disable "quickleave" + if conf.exists('disable-quickleave'): + igmp_proxy['disable_quickleave'] = True + + for intf in conf.list_nodes('interface'): + conf.set_level('protocols igmp-proxy interface {0}'.format(intf)) + interface = { + 'interface': intf, + 'alt_subnet': [], + 'role': 'downstream', + 'threshold': '1', + 'whitelist': [] + } + + if conf.exists('alt-subnet'): + interface['alt_subnet'] = conf.return_values('alt-subnet') + + if conf.exists('role'): + interface['role'] = conf.return_value('role') + + if conf.exists('threshold'): + interface['threshold'] = conf.return_value('threshold') + + if conf.exists('whitelist'): + interface['whitelist'] = conf.return_values('whitelist') + + # Append interface configuration to global configuration list + igmp_proxy['interface'].append(interface) + + return igmp_proxy + +def verify(igmp_proxy): + # bail out early - looks like removal from running config + if igmp_proxy is None: + return None + + # bail out early - service is disabled + if igmp_proxy['disable']: + return None + + # at least two interfaces are required, one upstream and one downstream + if len(igmp_proxy['interface']) < 2: + raise ConfigError('Must define an upstream and at least 1 downstream interface!') + + upstream = 0 + for i in igmp_proxy['interface']: + if "upstream" == i['role']: + upstream += 1 + + if upstream == 0: + raise ConfigError('At least 1 upstream interface is required!') + elif upstream > 1: + raise ConfigError('Only 1 upstream interface allowed!') + + return None + +def generate(igmp_proxy): + # bail out early - looks like removal from running config + if igmp_proxy is None: + return None + + # bail out early - service is disabled, but inform user + if igmp_proxy['disable']: + print('Warning: IGMP Proxy will be deactivated because it is disabled') + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(igmp_proxy) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(igmp_proxy): + if igmp_proxy is None or igmp_proxy['disable']: + # IGMP Proxy support is removed in the commit + os.system('sudo systemctl stop igmpproxy.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: + os.system('sudo systemctl restart igmpproxy.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index 474a6a5cf..cef735c0d 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -18,7 +18,7 @@ import sys import os - +import jinja2 import netifaces from vyos.config import Config @@ -26,60 +26,78 @@ from vyos import ConfigError config_file = r'/etc/default/mdns-repeater' -def get_config(): - interface_list = [] +config_tmpl = """ +### Autogenerated by mdns_repeater.py ### +DAEMON_ARGS="{{ interfaces | join(' ') }}" +""" + +default_config_data = { + 'disabled': False, + 'interfaces': [] +} +def get_config(): + mdns = default_config_data conf = Config() - conf.set_level('service mdns repeater') - if not conf.exists(''): - return interface_list + if not conf.exists('service mdns repeater'): + return None + else: + conf.set_level('service mdns repeater') - if conf.exists('interface'): - intfs_names = [] - intfs_names = conf.return_values('interface') + # Service can be disabled by user + if conf.exists('disable'): + mdns['disabled'] = True + return mdns - for name in intfs_names: - interface_list.append(name) + # Interface to repeat mDNS advertisements + if conf.exists('interface'): + mdns['interfaces'] = conf.return_values('interface') - return interface_list + return mdns def verify(mdns): - # '0' interfaces are possible, think of service deletion. Only '1' is not supported! - if len(mdns) == 1: - raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns)) - - # For mdns-repeater to work it is essential that the interfaces - # have an IP address assigned - for intf in mdns: - try: - netifaces.ifaddresses(intf)[netifaces.AF_INET] - except KeyError as e: - raise ConfigError('No IP address configured for interface "%s"!' % intf) + if mdns is None: + return None + + if mdns['disabled']: + return None + + # We need at least two interfaces to repeat mDNS advertisments + if len(mdns['interfaces']) < 2: + raise ConfigError('mDNS repeater requires at least 2 configured interfaces!') + + # For mdns-repeater to work it is essential that the interfaces has + # an IPv4 address assigned + for interface in mdns['interfaces']: + if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): + if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1: + raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) return None def generate(mdns): - config_header = '### Autogenerated by mdns_repeater.py ###\n' - if len(mdns) > 0: - config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n' - else: - config_args = 'DAEMON_ARGS=""\n' + if mdns is None: + return None + + if mdns['disabled']: + print('Warning: mDNS repeater will be deactivated because it is disabled') + return None - # write new configuration file - f = open(config_file, 'w') - f.write(config_header) - f.write(config_args) - f.close() + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(mdns) + with open(config_file, 'w') as f: + f.write(config_text) return None def apply(mdns): - if len(mdns) == 0: - cmd = "sudo systemctl stop mdns-repeater" + if (mdns is None) or mdns['disabled']: + os.system('sudo systemctl stop mdns-repeater') + if os.path.exists(config_file): + os.unlink(config_file) else: - cmd = "sudo systemctl restart mdns-repeater" + os.system('sudo systemctl restart mdns-repeater') - os.system(cmd) return None if __name__ == '__main__': diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 2a6088575..f706d502f 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -21,6 +21,7 @@ import os import jinja2 import ipaddress +import copy from vyos.config import Config from vyos import ConfigError @@ -36,12 +37,11 @@ config_tmpl = """ # driftfile /var/lib/ntp/ntp.drift # By default, only allow ntpd to query time sources, ignore any incoming requests -restrict default ignore +restrict default noquery nopeer notrap nomodify # Local users have unrestricted access, allowing reconfiguration via ntpdc restrict 127.0.0.1 restrict -6 ::1 - # # Configurable section # @@ -50,7 +50,6 @@ restrict -6 ::1 {% for s in servers -%} # Server configuration for: {{ s.name }} server {{ s.name }} iburst {{ s.options | join(" ") }} - {% endfor -%} {% endif %} @@ -79,7 +78,7 @@ default_config_data = { } def get_config(): - ntp = default_config_data + ntp = copy.deepcopy(default_config_data) conf = Config() if not conf.exists('system ntp'): return None @@ -108,8 +107,6 @@ def get_config(): "name": node, "options": [] } - if conf.exists('server {0} dynamic'.format(node)): - options.append('dynamic') if conf.exists('server {0} noselect'.format(node)): options.append('noselect') if conf.exists('server {0} preempt'.format(node)): @@ -154,10 +151,10 @@ def generate(ntp): def apply(ntp): if ntp is not None: - os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload') + os.system('sudo systemctl restart ntp.service') else: - # NTP suuport is removed in the commit - os.system('sudo /usr/sbin/invoke-rc.d ntp stop') + # NTP support is removed in the commit + os.system('sudo systemctl stop ntp.service') os.unlink(config_file) return None diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 3b47ffc98..06d2e253a 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -24,12 +24,12 @@ import pwd import time import jinja2 -import ipaddress import random import binascii import re import vyos.version +import vyos.validate from vyos.config import Config from vyos import ConfigError @@ -38,6 +38,7 @@ config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' +config_file_init = r'/etc/default/snmpd' # SNMP OIDs used to mark auth/priv type OIDs = { @@ -47,7 +48,7 @@ OIDs = { 'des' : '.1.3.6.1.6.3.10.1.2.2', 'none': '.1.3.6.1.6.3.10.1.2.1' } -# SNMPS template - be careful if you edit the template. +# SNMP template (/etc/snmp/snmp.conf) - be careful if you edit the template. client_config_tmpl = """ ### Autogenerated by snmp.py ### {% if trap_source -%} @@ -56,39 +57,38 @@ clientaddr {{ trap_source }} """ -# SNMPS template - be careful if you edit the template. +# SNMP template (/usr/share/snmp/snmpd.conf) - be careful if you edit the template. access_config_tmpl = """ ### Autogenerated by snmp.py ### -{% if v3_users %} -{% for u in v3_users %} +{%- for u in v3_users %} {{ u.mode }}user {{ u.name }} -{% endfor %} -{% endif -%} +{%- endfor %} + rwuser {{ vyos_user }} """ -# SNMPS template - be careful if you edit the template. +# SNMP template (/var/lib/snmp/snmpd.conf) - be careful if you edit the template. user_config_tmpl = """ ### Autogenerated by snmp.py ### # user -{% if v3_users %} -{% for u in v3_users %} -{% if u.authOID == 'none' %} +{%- for u in v3_users %} +{%- if u.authOID == 'none' %} createUser {{ u.name }} -{% elif u.authPassword %} +{%- elif u.authPassword %} createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} -{% else %} +{%- else %} usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x -{% endif %} -{% endfor %} -{% endif %} +{%- endif %} +{%- endfor %} createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +{%- if v3_engineid %} oldEngineID {{ v3_engineid }} +{%- endif %} """ -# SNMPS template - be careful if you edit the template. +# SNMP template (/etc/snmp/snmpd.conf) - be careful if you edit the template. daemon_config_tmpl = """ ### Autogenerated by snmp.py ### @@ -96,15 +96,10 @@ daemon_config_tmpl = """ sysObjectID 1.3.6.1.4.1.44641 sysServices 14 master agentx -agentXPerms 0755 0755 +agentXPerms 0777 0777 pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias smuxpeer .1.3.6.1.2.1.83 smuxpeer .1.3.6.1.2.1.157 -smuxpeer .1.3.6.1.4.1.3317.1.2.2 -smuxpeer .1.3.6.1.4.1.3317.1.2.3 -smuxpeer .1.3.6.1.4.1.3317.1.2.5 -smuxpeer .1.3.6.1.4.1.3317.1.2.8 -smuxpeer .1.3.6.1.4.1.3317.1.2.9 smuxsocket localhost # linkUp/Down configure the Event MIB tables to monitor @@ -122,110 +117,112 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 ######################## # configurable section # ######################## - {% if v3_tsm_key %} [snmp] localCert {{ v3_tsm_key }} -{% endif %} +{%- endif %} # Default system description is VyOS version sysDescr VyOS {{ version }} -{% if description -%} +{% if description %} # Description SysDescr {{ description }} -{% endif %} +{%- endif %} # Listen agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %} # SNMP communities -{% if communities -%} -{% for c in communities %} -{% if c.network -%} -{% for network in c.network %} +{%- for c in communities %} + +{%- if c.network_v4 %} +{%- for network in c.network_v4 %} {{ c.authorization }}community {{ c.name }} {{ network }} -{{ c.authorization }}community6 {{ c.name }} {{ network }} -{% endfor %} -{% else %} +{%- endfor %} +{%- elif not c.has_source %} {{ c.authorization }}community {{ c.name }} +{%- endif %} + +{%- if c.network_v6 %} +{%- for network in c.network_v6 %} +{{ c.authorization }}community6 {{ c.name }} {{ network }} +{%- endfor %} +{%- elif not c.has_source %} {{ c.authorization }}community6 {{ c.name }} -{% endif %} -{% endfor %} -{% endif %} +{%- endif %} + +{%- endfor %} -{% if contact -%} +{% if contact %} # system contact information SysContact {{ contact }} -{% endif %} +{%- endif %} -{% if location -%} +{% if location %} # system location information SysLocation {{ location }} -{% endif %} +{%- endif %} {% if smux_peers -%} # additional smux peers -{% for sp in smux_peers %} +{%- for sp in smux_peers %} smuxpeer {{ sp }} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endif %} {% if trap_targets -%} # if there is a problem - tell someone! -{% for t in trap_targets %} +{%- for t in trap_targets %} trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endif %} +{%- if v3_enabled %} # # SNMPv3 stuff goes here # -{% if v3_enabled %} - # views -{% if v3_views -%} -{% for v in v3_views %} -{% for oid in v.oids %} +{%- for v in v3_views %} +{%- for oid in v.oids %} view {{ v.name }} included .{{ oid.oid }} -{% endfor %} -{% endfor %} -{% endif %} +{%- endfor %} +{%- endfor %} # access # context sec.model sec.level match read write notif -{% if v3_groups -%} -{% for g in v3_groups %} -{% if g.mode == 'ro' %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none -access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none -{% elif g.mode == 'rw' %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none -access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {{ g.view }} none -{% endif %} -{% endfor -%} -{% endif %} +{%- for g in v3_groups %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none +{%- endfor %} # trap-target -{% if v3_traps -%} -{% for t in v3_traps %} +{%- for t in v3_traps %} trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{% endfor -%} -{% endif %} +{%- endfor %} # group -{% if v3_users -%} -{% for u in v3_users %} +{%- for u in v3_users %} group {{ u.group }} usm {{ u.name }} group {{ u.group }} tsm {{ u.name }} {% endfor %} -{% endif %} +{%- endif %} +""" -{% endif %} +# SNMP template (/etc/default/snmpd) - be careful if you edit the template. +init_config_tmpl = """ +### Autogenerated by snmp.py ### +# This file controls the activity of snmpd + +# snmpd control (yes means start daemon). +SNMPDRUN=yes +# snmpd options (use syslog, close stdin/out/err). +SNMPDOPTS='-LSed -u snmp -g snmp -p /run/snmpd.pid' """ default_config_data = { 'listen_on': [], + 'listen_address': [], 'communities': [], 'smux_peers': [], 'location' : '', @@ -271,14 +268,32 @@ def get_config(): community = { 'name': name, 'authorization': 'ro', - 'network': [] + 'network_v4': [], + 'network_v6': [], + 'has_source' : False } if conf.exists('community {0} authorization'.format(name)): community['authorization'] = conf.return_value('community {0} authorization'.format(name)) + # Subnet of SNMP client(s) allowed to contact system if conf.exists('community {0} network'.format(name)): - community['network'] = conf.return_values('community {0} network'.format(name)) + for addr in conf.return_values('community {0} network'.format(name)): + if vyos.validate.is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) + + # IP address of SNMP client allowed to contact system + if conf.exists('community {0} client'.format(name)): + for addr in conf.return_values('community {0} client'.format(name)): + if vyos.validate.is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) + + if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0): + community['has_source'] = True snmp['communities'].append(community) @@ -290,21 +305,20 @@ def get_config(): if conf.exists('listen-address'): for addr in conf.list_nodes('listen-address'): - listen = '' port = '161' if conf.exists('listen-address {0} port'.format(addr)): port = conf.return_value('listen-address {0} port'.format(addr)) - if ipaddress.ip_address(addr).version == 4: - # udp:127.0.0.1:161 - listen = 'udp:' + addr + ':' + port - elif ipaddress.ip_address(addr).version == 6: - # udp6:[::1]:161 - listen = 'udp6:' + '[' + addr + ']' + ':' + port - else: - raise ConfigError('Invalid IP address version') + snmp['listen_address'].append((addr, port)) - snmp['listen_on'].append(listen) + # Always listen on localhost if an explicit address has been configured + # This is a safety measure to not end up with invalid listen addresses + # that are not configured on this system. See https://phabricator.vyos.net/T850 + if not '127.0.0.1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('127.0.0.1', '161')) + + if not '::1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('::1', '161')) if conf.exists('location'): snmp['location'] = conf.return_value('location') @@ -579,6 +593,24 @@ def verify(snmp): if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + for listen in snmp['listen_address']: + addr = listen[0] + port = listen[1] + + if vyos.validate.is_ipv4(addr): + # example: udp:127.0.0.1:161 + listen = 'udp:' + addr + ':' + port + else: + # example: udp6:[::1]:161 + listen = 'udp6:' + '[' + addr + ']' + ':' + port + + # We only wan't to configure addresses that exist on the system. + # Hint the user if they don't exist + if vyos.validate.is_addr_assigned(addr): + snmp['listen_on'].append(listen) + else: + print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: # @@ -641,48 +673,45 @@ def verify(snmp): # Group must exist prior to mapping it into a group # seclevel will be extracted from group # - error = True if user['group']: + error = True if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: if group['name'] == user['group']: seclevel = group['seclevel'] error = False - if error: - raise ConfigError('You must create group "{0}" first'.format(user['group'])) + if error: + raise ConfigError('You must create group "{0}" first'.format(user['group'])) # Depending on the configured security level # the user has to provide additional info - if seclevel in ('auth', 'priv'): - if user['authPassword'] and user['authMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') + if user['authPassword'] and user['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') - if (not user['authPassword'] and not user['authMasterKey']): - raise ConfigError('Must specify encrypted-key or plaintext-key for user auth') + if (not user['authPassword'] and not user['authMasterKey']): + raise ConfigError('Must specify encrypted-key or plaintext-key for user auth') - # seclevel 'priv' is more restrictive - if seclevel in ('priv'): - if user['privPassword'] and user['privMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') + if user['privPassword'] and user['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') - if user['privPassword'] == '' and user['privMasterKey'] == '': - raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') + if user['privPassword'] == '' and user['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') - if user['privMasterKey'] and user['engineID'] == '': - raise ConfigError('Can not have "encrypted-key" without engineid') + if user['privMasterKey'] and user['engineID'] == '': + raise ConfigError('Can not have "encrypted-key" without engineid') - if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '': - raise ConfigError('Must specify auth or tsm-key for user auth') + if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '': + raise ConfigError('Must specify auth or tsm-key for user auth') - if user['mode'] == '': - raise ConfigError('Must specify user mode ro/rw') + if user['mode'] == '': + raise ConfigError('Must specify user mode ro/rw') - if user['privTsmKey']: - if not tsmKeyPattern.match(snmp['v3_tsm_key']): - if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): - if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): - raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + if user['privTsmKey']: + if not tsmKeyPattern.match(snmp['v3_tsm_key']): + if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): + if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): + raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') if 'v3_views' in snmp.keys(): for view in snmp['v3_views']: @@ -705,29 +734,35 @@ def generate(snmp): return None # Write client config file - tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(client_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_client, 'w') as f: f.write(config_text) # Write server config file - tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(daemon_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_daemon, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(access_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_access, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True) + tmpl = jinja2.Template(user_config_tmpl) config_text = tmpl.render(snmp) with open(config_file_user, 'w') as f: f.write(config_text) + # Write init config file + tmpl = jinja2.Template(init_config_tmpl) + config_text = tmpl.render(snmp) + with open(config_file_init, 'w') as f: + f.write(config_text) + return None def apply(snmp): @@ -761,9 +796,20 @@ def apply(snmp): # start SNMP daemon os.system("sudo systemctl restart snmpd.service") - # the passwords are not available immediately so this is a workaround - # and should be changed to polling - time.sleep(2) + # Passwords are not available immediately in the configuration file, + # after daemon startup - we wait until they have been processed by + # snmpd, which we see when a magic line appears in this file. + snmpReady = False + while not snmpReady: + while not os.path.exists(config_file_user): + time.sleep(1) + + with open(config_file_user, 'r') as f: + for line in f: + # Search for our magic string inside the file + if '**** DO NOT EDIT THIS FILE ****' in line: + snmpReady = True + break # Back in the Perl days the configuration was re-read and any # plaintext password inside the configuration was replaced by @@ -795,6 +841,9 @@ def apply(snmp): os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + # Enable AgentX in FRR + os.system('vtysh -c "configure terminal" -c "agentx" >/dev/null') + return None if __name__ == '__main__': diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index f1ac19473..2a5cba99a 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -67,82 +67,103 @@ UseDNS {{ host_validation }} # Specifies the port number that sshd listens on. The default is 22. # Multiple options of this type are permitted. +{% if mport|length != 0 %} +{% for p in mport %} +Port {{ p }} +{% endfor %} +{% else %} Port {{ port }} +{% endif %} # Gives the verbosity level that is used when logging messages from sshd LogLevel {{ log_level }} # Specifies whether root can log in using ssh -PermitRootLogin {{ allow_root }} +PermitRootLogin no # Specifies whether password authentication is allowed PasswordAuthentication {{ password_authentication }} -{% if listen_on -%} +{% if listen_on %} # Specifies the local addresses sshd should listen on -{% for a in listen_on -%} +{% for a in listen_on %} ListenAddress {{ a }} -{% endfor -%} +{% endfor %} +{{ "\n" }} {% endif %} -{% if ciphers -%} +{%- if ciphers %} # Specifies the ciphers allowed. Multiple ciphers must be comma-separated. # # NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ Ciphers {{ ciphers | join(",") }} +{{ "\n" }} {% endif %} -{% if mac -%} +{%- if mac %} # Specifies the available MAC (message authentication code) algorithms. The MAC # algorithm is used for data integrity protection. Multiple algorithms must be # comma-separated. # # NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ MACs {{ mac | join(",") }} +{{ "\n" }} {% endif %} -{% if key_exchange -%} +{%- if key_exchange %} # Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must # be comma-separated. # # NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ KexAlgorithms {{ key_exchange | join(",") }} +{{ "\n" }} {% endif %} -{% if allow_users -%} +{%- if allow_users %} # This keyword can be followed by a list of user name patterns, separated by spaces. # If specified, login is allowed only for user names that match one of the patterns. # Only user names are valid, a numerical user ID is not recognized. AllowUsers {{ allow_users | join(" ") }} +{{ "\n" }} {% endif %} -{% if allow_groups -%} +{%- if allow_groups %} # This keyword can be followed by a list of group name patterns, separated by spaces. # If specified, login is allowed only for users whose primary group or supplementary # group list matches one of the patterns. Only group names are valid, a numerical group # ID is not recognized. AllowGroups {{ allow_groups | join(" ") }} +{{ "\n" }} {% endif %} -{% if deny_users -%} +{%- if deny_users %} # This keyword can be followed by a list of user name patterns, separated by spaces. # Login is disallowed for user names that match one of the patterns. Only user names # are valid, a numerical user ID is not recognized. DenyUsers {{ deny_users | join(" ") }} +{{ "\n" }} {% endif %} -{% if deny_groups -%} +{%- if deny_groups %} # This keyword can be followed by a list of group name patterns, separated by spaces. # Login is disallowed for users whose primary group or supplementary group list matches # one of the patterns. Only group names are valid, a numerical group ID is not recognized. DenyGroups {{ deny_groups | join(" ") }} +{{ "\n" }} +{% endif %} + +{%- if client_keepalive %} +# Sets a timeout interval in seconds after which if no data has been received from the client, +# sshd will send a message through the encrypted channel to request a response from the client. +# The default is 0, indicating that these messages will not be sent to the client. +# This option applies to protocol version 2 only. +ClientAliveInterval {{ client_keepalive }} {% endif %} """ default_config_data = { 'port' : '22', 'log_level': 'INFO', - 'allow_root': 'no', 'password_authentication': 'yes', 'host_validation': 'yes' } @@ -171,9 +192,6 @@ def get_config(): deny_groups = conf.return_values('access-control deny group') ssh['deny_groups'] = deny_groups - if conf.exists('allow-root'): - ssh['allow-root'] = 'yes' - if conf.exists('ciphers'): ciphers = conf.return_values('ciphers') ssh['ciphers'] = ciphers @@ -208,8 +226,17 @@ def get_config(): ssh['mac'] = mac if conf.exists('port'): - port = conf.return_value('port') - ssh['port'] = port + ports = conf.return_values('port') + mport = [] + + for prt in ports: + mport.append(prt) + + ssh['mport'] = mport + + if conf.exists('client-keepalive-interval'): + client_keepalive = conf.return_value('client-keepalive-interval') + ssh['client_keepalive'] = client_keepalive return ssh @@ -228,7 +255,7 @@ def generate(ssh): if ssh is None: return None - tmpl = jinja2.Template(config_tmpl) + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) config_text = tmpl.render(ssh) with open(config_file, 'w') as f: f.write(config_text) @@ -236,10 +263,10 @@ def generate(ssh): def apply(ssh): if ssh is not None and 'port' in ssh.keys(): - os.system("sudo systemctl restart ssh") + os.system("sudo systemctl restart ssh.service") else: # SSH access is removed in the commit - os.system("sudo systemctl stop ssh") + os.system("sudo systemctl stop ssh.service") os.unlink(config_file) return None diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 5dfc6f390..d600146f3 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -30,6 +30,15 @@ from vyos import ConfigError configs = ''' ## generated by syslog.py ## ## file based logging +{% if files['global']['marker'] -%} +$ModLoad immark +{% if files['global']['marker-interval'] %} +$MarkMessagePeriod {{files['global']['marker-interval']}} +{% endif %} +{% endif -%} +{% if files['global']['preserver_fqdn'] -%} +$PreserveFQDN on +{% endif -%} {% for file in files %} $outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}} {{files[file]['selectors']}} :omfile:${{file}} @@ -80,10 +89,10 @@ def get_config(): c.set_level('system syslog') config_data = { - 'files' : {}, + 'files' : {}, 'console' : {}, - 'hosts' : {}, - 'user' : {} + 'hosts' : {}, + 'user' : {} } ##### @@ -93,21 +102,28 @@ def get_config(): config_data['files'].update( { 'global' : { - 'log-file' : '/var/log/vyos-rsyslog', - 'max-size' : 262144, + 'log-file' : '/var/log/messages', + 'max-size' : 262144, 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', - 'selectors' : '*.notice;local7.debug', - 'max-files' : '5' + 'selectors' : '*.notice;local7.debug', + 'max-files' : '5', + 'preserver_fqdn' : False } } ) + if c.exists('global marker'): + config_data['files']['global']['marker'] = True + if c.exists('global marker interval'): + config_data['files']['global']['marker-interval'] = c.return_value('global marker interval') if c.exists('global facility'): config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') if c.exists('global archive size'): config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 if c.exists('global archive files'): config_data['files']['global']['max-files'] = c.return_value('global archive files') + if c.exists('global preserve-fqdn'): + config_data['files']['global']['preserver_fqdn'] = True ### # set system syslog file @@ -215,26 +231,41 @@ def generate_selectors(c, config_node): return selectors def generate(c): + if c == None: + return None + tmpl = jinja2.Template(configs, trim_blocks=True) config_text = tmpl.render(c) - #print (config_text) with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: f.write(config_text) ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) config_text = tmpl.render(c) - #print (config_text) with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: f.write(config_text) def verify(c): if c == None: return None + # + # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) + # it interferes with the global logging, to make sure we are using a single base, template is enforced here + # + if not os.path.islink('/etc/rsyslog.conf'): + os.remove('/etc/rsyslog.conf') + os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + + # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there + # is a chance that someone still needs it, so I don't automatically remove them + + if c == None: + return None fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\ 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7'] lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*'] + for conf in c: if c[conf]: for item in c[conf]: @@ -250,10 +281,16 @@ def verify(c): def apply(c): ### vyatta-log.conf is being generated somewhere ### this is just a quick hack to remove the old configfile - - if os.path.exists('/etc/rsyslog.d/vyatta-log.conf'): - os.remove('/etc/rsyslog.d/vyatta-log.conf') - os.system("sudo systemctl restart rsyslog >/dev/null") + + if c == None: + ### systemd restarts it, using kill + #os.system("sudo systemctl stop rsyslog >/dev/null 2>&1") + print ("systemd sends messages to rsyslog, rsyslog won't be stopped") + else: + if os.path.exists('/etc/rsyslog.d/vyatta-log.conf'): + os.remove('/etc/rsyslog.d/vyatta-log.conf') + + os.system("sudo systemctl restart rsyslog >/dev/null") if __name__ == '__main__': try: diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py index 285afe2b5..b171e9576 100755 --- a/src/conf_mode/task_scheduler.py +++ b/src/conf_mode/task_scheduler.py @@ -49,7 +49,7 @@ def make_command(executable, arguments): if arguments: return("sg vyattacfg \"{0} {1}\"".format(executable, arguments)) else: - return(executable) + return("sg vyattacfg \"{0}\"".format(executable)) def get_config(): conf = Config() diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py new file mode 100755 index 000000000..ff7cad0c9 --- /dev/null +++ b/src/conf_mode/tftp_server.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import stat +import pwd +import copy +import glob + +import jinja2 +import vyos.validate + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/default/tftpd' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by tftp_server.py ### +DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}" + + +""" + +default_config_data = { + 'directory': '', + 'allow_upload': False, + 'port': '69', + 'listen': [] +} + +def get_config(): + tftpd = copy.deepcopy(default_config_data) + conf = Config() + if not conf.exists('service tftp-server'): + return None + else: + conf.set_level('service tftp-server') + + if conf.exists('directory'): + tftpd['directory'] = conf.return_value('directory') + + if conf.exists('allow-upload'): + tftpd['allow_upload'] = True + + if conf.exists('port'): + tftpd['port'] = conf.return_value('port') + + tftpd['listen'] = conf.return_values('listen-address') + + return tftpd + +def verify(tftpd): + # bail out early - looks like removal from running config + if tftpd is None: + return None + + # Configuring allowed clients without a server makes no sense + if not tftpd['directory']: + raise ConfigError('TFTP root directory must be configured!') + + if not tftpd['listen']: + raise ConfigError('TFTP server listen address must be configured!') + + for addr in tftpd['listen']: + if not vyos.validate.is_addr_assigned(addr): + print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr)) + + return None + +def generate(tftpd): + # cleanup any available configuration file + # files will be recreated on demand + for i in glob.glob(config_file + '*'): + os.unlink(i) + + # bail out early - looks like removal from running config + if tftpd is None: + return None + + idx = 0 + for listen in tftpd['listen']: + config = copy.deepcopy(tftpd) + if vyos.validate.is_ipv4(listen): + config['listen'] = [listen + ":" + tftpd['port'] + " -4"] + else: + config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(config) + file = config_file + str(idx) + with open(file, 'w') as f: + f.write(config_text) + + idx = idx + 1 + + return None + +def apply(tftpd): + # stop all services first - then we will decide + os.system('sudo systemctl stop tftpd@{0..20}') + + # bail out early - e.g. service deletion + if tftpd is None: + return None + + tftp_root = tftpd['directory'] + if not os.path.exists(tftp_root): + os.makedirs(tftp_root) + os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) + + # get UNIX uid for user 'tftp' + tftp_uid = pwd.getpwnam('tftp').pw_uid + tftp_gid = pwd.getpwnam('tftp').pw_gid + + # get UNIX uid for tftproot directory + dir_uid = os.stat(tftp_root).st_uid + dir_gid = os.stat(tftp_root).st_gid + + # adjust uid/gid of tftproot directory if files don't belong to user tftp + if (tftp_uid != dir_uid) or (tftp_gid != dir_gid): + os.chown(tftp_root, tftp_uid, tftp_gid) + + idx = 0 + for listen in tftpd['listen']: + os.system('sudo systemctl restart tftpd@{0}.service'.format(idx)) + idx = idx + 1 + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d21e3ef40..bc833b63f 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -76,7 +76,10 @@ vrrp_instance {{ group.name }} { {%- endif %} {% endif -%} - {% if group.use_vmac -%} + {% if group.use_vmac and group.peer_address -%} + use_vmac {{group.interface}}v{{group.vrid}} + vmac_xmit_base + {% elif group.use_vmac -%} use_vmac {{group.interface}}v{{group.vrid}} {% endif -%} @@ -183,7 +186,7 @@ def get_config(): if not group["priority"]: group["priority"] = 100 if not group["preempt_delay"]: - group["preempt_delay"] = 5 * 60 + group["preempt_delay"] = 0 if not group["health_check_interval"]: group["health_check_interval"] = 60 if not group["health_check_count"]: @@ -273,7 +276,7 @@ def verify(data): count = len(_groups) - 1 index = 0 while (index < count): - if _groups[index]["vrid"] == _groups[index + 1]["vrid"]: + if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]): raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format( _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"])) else: diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py new file mode 100755 index 000000000..e893dba47 --- /dev/null +++ b/src/conf_mode/wireguard.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import syslog as sl +import subprocess + +from vyos.config import Config +from vyos import ConfigError + +dir = r'/config/auth/wireguard' +pk = dir + '/private.key' +pub = dir + '/public.key' +psk_file = r'/tmp/psk' + +def check_kmod(): + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") + +def get_config(): + c = Config() + if not c.exists('interfaces wireguard'): + return None + + c.set_level('interfaces') + intfcs = c.list_nodes('wireguard') + intfcs_eff = c.list_effective_nodes('wireguard') + new_lst = list(set(intfcs) - set(intfcs_eff)) + del_lst = list(set(intfcs_eff) - set(intfcs)) + + config_data = { + 'interfaces' : {} + } + ### setting defaults and determine status of the config + for intfc in intfcs: + cnf = 'wireguard ' + intfc + # default data struct + config_data['interfaces'].update( + { + intfc : { + 'addr' : '', + 'descr' : intfc, ## snmp ifAlias + 'lport' : '', + 'status' : 'exists', + 'state' : 'enabled', + 'mtu' : '1420', + 'peer' : {} + } + } + ) + + ### determine status either delete or create + for i in new_lst: + config_data['interfaces'][i]['status'] = 'create' + + for i in del_lst: + config_data['interfaces'].update( + { + i : { + 'status': 'delete' + } + } + ) + + ### based on the status, setup conf values + for intfc in intfcs: + cnf = 'wireguard ' + intfc + if config_data['interfaces'][intfc]['status'] != 'delete': + ### addresses + if c.exists(cnf + ' address'): + config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') + ### interface up/down + if c.exists(cnf + ' disable'): + config_data['interfaces'][intfc]['state'] = 'disable' + ### listen port + if c.exists(cnf + ' port'): + config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') + ### description + if c.exists(cnf + ' description'): + config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') + ### mtu + if c.exists(cnf + ' mtu'): + config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') + ### peers + if c.exists(cnf + ' peer'): + for p in c.list_nodes(cnf + ' peer'): + if not c.exists(cnf + ' peer ' + p + ' disable'): + config_data['interfaces'][intfc]['peer'].update( + { + p : { + 'allowed-ips' : [], + 'endpoint' : '', + 'pubkey' : '' + } + } + ) + if c.exists(cnf + ' peer ' + p + ' pubkey'): + config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') + if c.exists(cnf + ' peer ' + p + ' allowed-ips'): + config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') + if c.exists(cnf + ' peer ' + p + ' endpoint'): + config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') + if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): + config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') + if c.exists(cnf + ' peer ' + p + ' preshared-key'): + config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') + + return config_data + +def verify(c): + if not c: + return None + + for i in c['interfaces']: + if c['interfaces'][i]['status'] != 'delete': + if not c['interfaces'][i]['addr']: + raise ConfigError("address required for interface " + i) + if not c['interfaces'][i]['peer']: + raise ConfigError("peer required on interface " + i) + + for p in c['interfaces'][i]['peer']: + if not c['interfaces'][i]['peer'][p]['allowed-ips']: + raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) + if not c['interfaces'][i]['peer'][p]['pubkey']: + raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) + + +def apply(c): + ### no wg config left, delete all wireguard devices on the os + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() + if re.search("DEVTYPE=wireguard", buf, re.I|re.M): + wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) + sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) + subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + ### + ## find the diffs between effective config an new config + ### + c_eff = Config() + c_eff.set_level('interfaces wireguard') + + ### link status up/down aka interface disable + + for intf in c['interfaces']: + if not c['interfaces'][intf]['status'] == 'delete': + if c['interfaces'][intf]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) + subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) + else: + sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) + subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) + + ### deletion of a specific interface + for intf in c['interfaces']: + if c['interfaces'][intf]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) + subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) + + ### peer deletion + peer_eff = c_eff.list_effective_nodes( intf + ' peer') + peer_cnf = [] + try: + for p in c['interfaces'][intf]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') + remove_peer(intf, pkey) + + ### peer pubkey update + ### wg identifies peers by its pubky, so we have to remove the peer first + ### it will recreated it then below with the new key from the cli config + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') + nkey = c['interfaces'][intf]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) + remove_peer(intf, ekey) + + ### new config + if c['interfaces'][intf]['status'] == 'create': + if not os.path.exists(pk): + raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + + subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) + for addr in c['interfaces'][intf]['addr']: + add_addr(intf, addr) + + subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) + configure_interface(c, intf) + + ### config updates + if c['interfaces'][intf]['status'] == 'exists': + ### IP address change + addr_eff = re.sub("\'", "", c_eff.return_effective_values(intf + ' address')).split() + addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) + addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) + + if len(addr_rem) != 0: + for addr in addr_rem: + del_addr(intf, addr) + + if len(addr_add) != 0: + for addr in addr_add: + add_addr(intf, addr) + + ## mtu update + mtu = c['interfaces'][intf]['mtu'] + if mtu != 1420: + sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) + subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) + + + ### persistent-keepalive + for p in c['interfaces'][intf]['peer']: + val_eff = "" + val = "" + + try: + val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + except KeyError: + pass + + if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): + val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') + + ### disable keepalive + if val_eff and not val: + c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 + + ### set new keepalive value + if not val_eff and val: + c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val + + ## wg command call + configure_interface(c, intf) + + ### ifalias for snmp from description + if c['interfaces'][intf]['status'] != 'delete': + descr_eff = c_eff.return_effective_value(intf + ' description') + cnf_descr = c['interfaces'][intf]['descr'] + if descr_eff != cnf_descr: + with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: + fh.write(str(cnf_descr)) + +def configure_interface(c, intf): + for p in c['interfaces'][intf]['peer']: + ## config init for wg call + wg_config = { + 'interface' : intf, + 'port' : 0, + 'private-key' : pk, + 'pubkey' : '', + 'psk' : '/dev/null', + 'allowed-ips' : [], + 'fwmark' : 0x00, + 'endpoint' : None, + 'keepalive' : 0 + } + + ## mandatory settings + wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] + wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] + + ## optional settings + # listen-port + if c['interfaces'][intf]['lport']: + wg_config['port'] = c['interfaces'][intf]['lport'] + + ## endpoint + if c['interfaces'][intf]['peer'][p]['endpoint']: + wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] + + ## persistent-keepalive + if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: + wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + + ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either + if 'psk' in c['interfaces'][intf]['peer'][p]: + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) + os.umask(old_umask) + wg_config['psk'] = psk_file + + ### assemble wg command + cmd = "sudo wg set " + intf + cmd += " listen-port " + str(wg_config['port']) + cmd += " private-key " + wg_config['private-key'] + cmd += " peer " + wg_config['pubkey'] + cmd += " preshared-key " + wg_config['psk'] + cmd += " allowed-ips " + for ap in wg_config['allowed-ips']: + if ap != wg_config['allowed-ips'][-1]: + cmd += ap + "," + else: + cmd += ap + + if wg_config['endpoint']: + cmd += " endpoint " + wg_config['endpoint'] + + if wg_config['keepalive'] != 0: + cmd += " persistent-keepalive " + wg_config['keepalive'] + else: + cmd += " persistent-keepalive 0" + + sl.syslog(sl.LOG_NOTICE, cmd) + #print (cmd) + subprocess.call([cmd], shell=True) + """ remove psk_file """ + if os.path.exists(psk_file): + os.remove(psk_file) + +def add_addr(intf, addr): + # see https://phabricator.vyos.net/T949 + ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) + sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) + +def del_addr(intf, addr): + ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) + sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) + +def remove_peer(intf, peer_key): + cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' + ret = subprocess.call([cmd], shell=True) + sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) + +if __name__ == '__main__': + try: + check_kmod() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/etc/init.d/igmpproxy b/src/etc/init.d/igmpproxy new file mode 100755 index 000000000..4a2c94a4d --- /dev/null +++ b/src/etc/init.d/igmpproxy @@ -0,0 +1,166 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: igmpproxy +# Required-Start: $local_fs $network $remote_fs $syslog +# Required-Stop: $local_fs $network $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: IGMP multicast routing daemon +# Description: IGMPproxy is a simple dynamic Multicast Routing Daemon +# using only IGMP signalling. It's intended for simple +# forwarding of Multicast traffic between networks. +### END INIT INFO + +# Author: Pali Rohár <pali.rohar@gmail.com> + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="igmpproxy" +NAME=igmpproxy +DAEMON=/sbin/igmpproxy +DAEMON_ARGS="/etc/igmpproxy.conf" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -b -m -- \ + $DAEMON_OPTS $DAEMON_ARGS \ + || return 2 + # The above code will not work for interpreted scripts, use the next + # six lines below instead (Ref: #643337, start-stop-daemon(8) ) + #start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \ + # --name $NAME --test > /dev/null \ + # || return 1 + #start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \ + # --name $NAME -- $DAEMON_ARGS \ + # || return 2 + + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/src/etc/init.d/isc-dhcpv6-relay b/src/etc/init.d/isc-dhcpv6-relay new file mode 100755 index 000000000..5a8ce620c --- /dev/null +++ b/src/etc/init.d/isc-dhcpv6-relay @@ -0,0 +1,50 @@ +#!/bin/sh +# +# + +### BEGIN INIT INFO +# Provides: isc-dhcpv6-relay +# Required-Start: $remote_fs $network +# Required-Stop: $remote_fs $network +# Should-Start: $local_fs +# Should-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: IPv6 DHCP relay +# Description: Dynamic Host Configuration Protocol Relay for IPv6 +### END INIT INFO + +# It is not safe to start if we don't have a default configuration... +if [ ! -f /etc/default/isc-dhcpv6-relay ]; then + echo "/etc/default/isc-dhcpv6-relay does not exist! - Aborting..." + exit 1 +fi + +# Source init functions +. /lib/lsb/init-functions + +# Read init script configuration (interfaces the daemon should listen on +# and the DHCP server we should forward requests to.) +[ -f /etc/default/isc-dhcpv6-relay ] && . /etc/default/isc-dhcpv6-relay + +DHCRELAYPID=/var/run/dhcv6relay.pid + +case "$1" in + start) + start-stop-daemon --start --quiet --pidfile $DHCRELAYPID \ + --exec /usr/sbin/dhcrelay -- -q $OPTIONS -pf $DHCRELAYPID + ;; + stop) + start-stop-daemon --stop --quiet --pidfile $DHCRELAYPID + ;; + restart | force-reload) + $0 stop + sleep 2 + $0 start + ;; + *) + echo "Usage: /etc/init.d/isc-dhcpv6-relay {start|stop|restart|force-reload}" + exit 1 +esac + +exit 0 diff --git a/src/etc/init.d/isc-dhcpv6-server b/src/etc/init.d/isc-dhcpv6-server new file mode 100755 index 000000000..441827d5f --- /dev/null +++ b/src/etc/init.d/isc-dhcpv6-server @@ -0,0 +1,113 @@ +#!/bin/sh +# +# + +### BEGIN INIT INFO +# Provides: isc-dhcpv6-server +# Required-Start: $remote_fs $network $syslog +# Required-Stop: $remote_fs $network $syslog +# Should-Start: $local_fs slapd $named +# Should-Stop: $local_fs slapd +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: IPv6 DHCP server +# Description: Dynamic Host Configuration Protocol Server for IPv6 +### END INIT INFO + +PATH=/sbin:/bin:/usr/sbin:/usr/bin + +test -f /usr/sbin/dhcpd || exit 0 + +DHCPD_DEFAULT="${DHCPD_DEFAULT:-/etc/default/isc-dhcpv6-server}" + +# It is not safe to start if we don't have a default configuration... +if [ ! -f "$DHCPD_DEFAULT" ]; then + echo "$DHCPD_DEFAULT does not exist! - Aborting..." + exit 0 +fi + +. /lib/lsb/init-functions + +# Read init script configuration +[ -f "$DHCPD_DEFAULT" ] && . "$DHCPD_DEFAULT" + +NAME=dhcpd +DESC="ISC DHCP server" +# fallback to default config file +DHCPD_CONF=${DHCPD_CONF:-/etc/dhcp/dhcpd.conf} +# try to read pid file name from config file, with fallback to /var/run/dhcpd.pid +if [ -z "$DHCPD_PID" ]; then + DHCPD_PID=$(sed -n -e 's/^[ \t]*pid-file-name[ \t]*"(.*)"[ \t]*;.*$/\1/p' < "$DHCPD_CONF" 2>/dev/null | head -n 1) +fi +DHCPD_PID="${DHCPD_PID:-/var/run/dhcpd.pid}" + +test_config() +{ + if ! /usr/sbin/dhcpd -t $OPTIONS -q -cf "$DHCPD_CONF" > /dev/null 2>&1; then + echo "dhcpd self-test failed. Please fix $DHCPD_CONF." + echo "The error was: " + /usr/sbin/dhcpd -t $OPTIONS -cf "$DHCPD_CONF" + exit 1 + fi + touch /var/lib/dhcp/dhcpd.leases +} + +# single arg is -v for messages, -q for none +check_status() +{ + if [ ! -r "$DHCPD_PID" ]; then + test "$1" != -v || echo "$NAME is not running." + return 3 + fi + if read pid < "$DHCPD_PID" && ps -p "$pid" > /dev/null 2>&1; then + test "$1" != -v || echo "$NAME is running." + return 0 + else + test "$1" != -v || echo "$NAME is not running but $DHCPD_PID exists." + return 1 + fi +} + +case "$1" in + start) + test_config + log_daemon_msg "Starting $DESC" "$NAME" + start-stop-daemon --start --quiet --pidfile "$DHCPD_PID" \ + --exec /usr/sbin/dhcpd -- \ + -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES + sleep 2 + + if check_status -q; then + log_end_msg 0 + else + log_failure_msg "check syslog for diagnostics." + log_end_msg 1 + exit 1 + fi + ;; + stop) + log_daemon_msg "Stopping $DESC" "$NAME" + start-stop-daemon --stop --quiet --pidfile "$DHCPD_PID" + log_end_msg $? + rm -f "$DHCPD_PID" + ;; + restart | force-reload) + test_config + $0 stop + sleep 2 + $0 start + if [ "$?" != "0" ]; then + exit 1 + fi + ;; + status) + echo -n "Status of $DESC: " + check_status -v + exit "$?" + ;; + *) + echo "Usage: $0 {start|stop|restart|force-reload|status}" + exit 1 +esac + +exit 0 diff --git a/src/helpers/commands-pipe.py b/src/helpers/commands-pipe.py deleted file mode 100755 index 1120bb09e..000000000 --- a/src/helpers/commands-pipe.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python3 - -import sys -import re - -from signal import signal, SIGPIPE, SIG_DFL -from vyos.configtree import ConfigTree - -signal(SIGPIPE,SIG_DFL) - -config_string = sys.stdin.read().strip() - -if not config_string: - sys.exit(0) - -# When used in conf mode pipe, the config given to the script is likely incomplete -# and breaks the "all top level nodes are neither tag nor leaf" -# invariant, so we wrap it into a fake node. -# Since nodes don't normally start with an underscore, -# __root__ is hygienic enough. -config_string = "__root__ {{ {0} \n }}".format(config_string) - -config_re = re.compile(r'(set|comment)\s+__root__\s+(.*)') - -config = ConfigTree(config_string) -commands = config.to_commands() -commands = config_re.sub("\\1 \\2", commands) - -print(commands) diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py index d702739b5..36f996d38 100755 --- a/src/helpers/validate-value.py +++ b/src/helpers/validate-value.py @@ -23,7 +23,7 @@ try: except Exception as exn: if debug: print(exn) - else: + else: pass try: diff --git a/src/migration-scripts/dhcp-relay/1-to-2 b/src/migration-scripts/dhcp-relay/1-to-2 new file mode 100755 index 000000000..b72da1028 --- /dev/null +++ b/src/migration-scripts/dhcp-relay/1-to-2 @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +# Delete "set service dhcp-relay relay-options port" option +# Delete "set service dhcpv6-relay listen-port" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not (config.exists(['service', 'dhcp-relay', 'relay-options', 'port']) or config.exists(['service', 'dhcpv6-relay', 'listen-port'])): + # Nothing to do + sys.exit(0) +else: + # Delete abandoned node + config.delete(['service', 'dhcp-relay', 'relay-options', 'port']) + # Delete abandoned node + config.delete(['service', 'dhcpv6-relay', 'listen-port']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5 new file mode 100755 index 000000000..baccd5a2b --- /dev/null +++ b/src/migration-scripts/dhcp-server/4-to-5 @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +# Removes boolean operator from: +# - "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable (true|false)" +# - "set service dhcp-server shared-network-name <xyz> authoritative (true|false)" +# - "set service dhcp-server disabled (true|false)" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'dhcp-server']): + # Nothing to do + sys.exit(0) +else: + base = ['service', 'dhcp-server'] + # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless + if config.exists(base + ['dynamic-dns-update']): + bool_val = config.return_value(base + ['dynamic-dns-update', 'enable']) + + # Delete the node with the old syntax + config.delete(base + ['dynamic-dns-update']) + if str(bool_val) == 'true': + # Enable dynamic-dns-update with new syntax + config.set(base + ['dynamic-dns-update'], value=None) + + # Make node "set service dhcp-server disabled (true|false)" valueless + if config.exists(base + ['disabled']): + bool_val = config.return_value(base + ['disabled']) + + # Delete the node with the old syntax + config.delete(base + ['disabled']) + if str(bool_val) == 'true': + # Now disable DHCP server with the new syntax + config.set(base + ['disable'], value=None) + + # Make node "set service dhcp-server hostfile-update (enable|disable) valueless + if config.exists(base + ['hostfile-update']): + bool_val = config.return_value(base + ['hostfile-update']) + + # Delete the node with the old syntax incl. all subnodes + config.delete(base + ['hostfile-update']) + if str(bool_val) == 'enable': + # Enable hostfile update with new syntax + config.set(base + ['hostfile-update'], value=None) + + # Run this for every instance if 'shared-network-name' + for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] + # format as tag node to avoid loading problems + config.set_tag(base + ['shared-network-name']) + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + # format as tag node to avoid loading problems + config.set_tag(base_network + ['subnet']) + + # Make node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable" valueless + if config.exists(base_subnet + ['ip-forwarding', 'enable']): + bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable']) + # Delete the node with the old syntax + config.delete(base_subnet + ['ip-forwarding']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(base_subnet + ['ip-forwarding'], value=None) + + # Rename node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9> + if config.exists(base_subnet + ['start']): + # This is the new "range" id for DHCP lease ranges + r_id = 0 + for range in config.list_nodes(base_subnet + ['start']): + range_start = range + range_stop = config.return_value(base_subnet + ['start', range_start, 'stop']) + + # Delete the node with the old syntax + config.delete(base_subnet + ['start', range_start]) + + # Create the node for the new syntax + # Note: range is a tag node, counter is its child, not a value + config.set(base_subnet + ['range', r_id]) + config.set(base_subnet + ['range', r_id, 'start'], value=range_start) + config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop) + + # format as tag node to avoid loading problems + config.set_tag(base_subnet + ['range']) + + # increment range id for possible next range definition + r_id += 1 + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start']) + + + # Make node "set service dhcp-server shared-network-name <xyz> authoritative" valueless + if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']): + authoritative = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + # Recreate node with new syntax - if required + if authoritative == "enable": + config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + + sys.exit(1) diff --git a/src/migration-scripts/ipsec/4-to-5 b/src/migration-scripts/ipsec/4-to-5 new file mode 100755 index 000000000..b64aa8462 --- /dev/null +++ b/src/migration-scripts/ipsec/4-to-5 @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# log-modes have changed, keyword all to any + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) + +if not ctree.exists(['vpn', 'ipsec', 'logging','log-modes']): + # Nothing to do + sys.exit(0) +else: + lmodes = ctree.return_values(['vpn', 'ipsec', 'logging','log-modes']) + for mode in lmodes: + if mode == 'all': + ctree.set(['vpn', 'ipsec', 'logging','log-modes'], value='any', replace=True) + + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/l2tp/0-to-1 b/src/migration-scripts/l2tp/0-to-1 new file mode 100755 index 000000000..f6c716df1 --- /dev/null +++ b/src/migration-scripts/l2tp/0-to-1 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Unclutter L2TP VPN configuiration - move radius-server top level tag +# nodes to a regular node which now also configures the radius source address +# used when querying a radius server + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'l2tp', 'remote-access', 'authentication'] +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + # Migrate "vpn l2tp authentication radius-source-address" to new + # "vpn l2tp authentication radius source-address" + if config.exists(cfg_base + ['radius-source-address']): + address = config.return_value(cfg_base + ['radius-source-address']) + # delete old configuration node + config.delete(cfg_base + ['radius-source-address']) + # write new configuration node + config.set(cfg_base + ['radius', 'source-address'], value=address) + + # Migrate "vpn l2tp authentication radius-server" tag node to new + # "vpn l2tp authentication radius server" tag node + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + key = config.return_value(base_server + ['key']) + + # delete old configuration node + config.delete(base_server) + # write new configuration node + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/ntp/0-to-1 b/src/migration-scripts/ntp/0-to-1 new file mode 100755 index 000000000..9c66f3109 --- /dev/null +++ b/src/migration-scripts/ntp/0-to-1 @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Delete "set system ntp server <n> dynamic" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'ntp']): + # Nothing to do + sys.exit(0) +else: + # Delete abandoned leaf node if found inside tag node for + # "set system ntp server <n> dynamic" + base = ['system', 'ntp', 'server'] + for server in config.list_nodes(base): + if config.exists(base + [server, 'dynamic']): + config.delete(base + [server, 'dynamic']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pppoe-server/0-to-1 b/src/migration-scripts/pppoe-server/0-to-1 new file mode 100755 index 000000000..bb24211b6 --- /dev/null +++ b/src/migration-scripts/pppoe-server/0-to-1 @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +# Convert "service pppoe-server authentication radius-server node key" +# to: +# "service pppoe-server authentication radius-server node secret" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) + + +if not ctree.exists(['service', 'pppoe-server', 'authentication','radius-server']): + # Nothing to do + sys.exit(0) +else: + nodes = ctree.list_nodes(['service', 'pppoe-server', 'authentication','radius-server']) + for node in nodes: + if ctree.exists(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']): + val = ctree.return_value(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']) + ctree.set(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'secret'], value=val, replace=False) + ctree.delete(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']) + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pptp/0-to-1 b/src/migration-scripts/pptp/0-to-1 new file mode 100755 index 000000000..d0c7a83b5 --- /dev/null +++ b/src/migration-scripts/pptp/0-to-1 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Unclutter PPTP VPN configuiration - move radius-server top level tag +# nodes to a regular node which now also configures the radius source address +# used when querying a radius server + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'pptp', 'remote-access', 'authentication'] +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + # Migrate "vpn pptp authentication radius-source-address" to new + # "vpn pptp authentication radius source-address" + if config.exists(cfg_base + ['radius-source-address']): + address = config.return_value(cfg_base + ['radius-source-address']) + # delete old configuration node + config.delete(cfg_base + ['radius-source-address']) + # write new configuration node + config.set(cfg_base + ['radius', 'source-address'], value=address) + + # Migrate "vpn pptp authentication radius-server" tag node to new + # "vpn pptp authentication radius server" tag node + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + key = config.return_value(base_server + ['key']) + + # delete old configuration node + config.delete(base_server) + # write new configuration node + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/quagga/2-to-3 b/src/migration-scripts/quagga/2-to-3 new file mode 100755 index 000000000..4c1cd86a3 --- /dev/null +++ b/src/migration-scripts/quagga/2-to-3 @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +def migrate_neighbor(config, neighbor_path, neighbor): + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + # Move the valueless options: as-override, next-hop-self, route-reflector-client, route-server-client, + # remove-private-as + for valueless_option in ['as-override', 'nexthop-self', 'route-reflector-client', 'route-server-client', + 'remove-private-as']: + if config.exists(neighbor_path + [neighbor, valueless_option]): + config.set(neighbor_path + [neighbor] + af_path + [valueless_option]) + config.delete(neighbor_path + [neighbor, valueless_option]) + + # Move filter options: distribute-list, filter-list, prefix-list, and route-map + # They share the same syntax inside so we can group them + for filter_type in ['distribute-list', 'filter-list', 'prefix-list', 'route-map']: + if config.exists(neighbor_path + [neighbor, filter_type]): + for filter_dir in ['import', 'export']: + if config.exists(neighbor_path + [neighbor, filter_type, filter_dir]): + filter_name = config.return_value(neighbor_path + [neighbor, filter_type, filter_dir]) + config.set(neighbor_path + [neighbor] + af_path + [filter_type, filter_dir], value=filter_name) + config.delete(neighbor_path + [neighbor, filter_type]) + + # Move simple leaf node options: maximum-prefix, unsuppress-map, weight + for leaf_option in ['maximum-prefix', 'unsuppress-map', 'weight']: + if config.exists(neighbor_path + [neighbor, leaf_option]): + if config.exists(neighbor_path + [neighbor, leaf_option]): + leaf_opt_value = config.return_value(neighbor_path + [neighbor, leaf_option]) + config.set(neighbor_path + [neighbor] + af_path + [leaf_option], value=leaf_opt_value) + config.delete(neighbor_path + [neighbor, leaf_option]) + + # The rest is special cases, for better or worse + + # Move allowas-in + if config.exists(neighbor_path + [neighbor, 'allowas-in']): + if config.exists(neighbor_path + [neighbor, 'allowas-in', 'number']): + allowas_in = config.return_value(neighbor_path + [neighbor, 'allowas-in', 'number']) + config.set(neighbor_path + [neighbor] + af_path + ['allowas-in', 'number'], value=allowas_in) + config.delete(neighbor_path + [neighbor, 'allowas-in']) + + # Move attribute-unchanged options + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged']): + for attr in ['as-path', 'med', 'next-hop']: + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged', attr]): + config.set(neighbor_path + [neighbor] + af_path + ['attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged']) + + # Move capability options + if config.exists(neighbor_path + [neighbor, 'capability']): + # "capability dynamic" is a peer-global option, we only migrate ORF + if config.exists(neighbor_path + [neighbor, 'capability', 'orf']): + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']): + for orf in ['send', 'receive']: + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]): + config.set(neighbor_path + [neighbor] + af_path + ['capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']) + config.delete(neighbor_path + [neighbor, 'capability', 'orf']) + + # Move default-originate + if config.exists(neighbor_path + [neighbor, 'default-originate']): + if config.exists(neighbor_path + [neighbor, 'default-originate', 'route-map']): + route_map = config.return_value(neighbor_path + [neighbor, 'default-originate', 'route-map']) + config.set(neighbor_path + [neighbor] + af_path + ['default-originate', 'route-map'], value=route_map) + else: + # Empty default-originate node is meaningful so we re-create it + config.set(neighbor_path + [neighbor] + af_path + ['default-originate']) + config.delete(neighbor_path + [neighbor, 'default-originate']) + + # Move soft-reconfiguration + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration']): + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration', 'inbound']): + config.set(neighbor_path + [neighbor] + af_path + ['soft-reconfiguration', 'inbound']) + # Empty soft-reconfiguration is meaningless, so we just remove it + config.delete(neighbor_path + [neighbor, 'soft-reconfiguration']) + + # Move disable-send-community + if config.exists(neighbor_path + [neighbor, 'disable-send-community']): + for comm_type in ['standard', 'extended']: + if config.exists(neighbor_path + [neighbor, 'disable-send-community', comm_type]): + config.set(neighbor_path + [neighbor] + af_path + ['disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community']) + + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Just to avoid writing it so many times + af_path = ['address-family', 'ipv4-unicast'] + + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + bgp_path = ['protocols', 'bgp', asn] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + ## Move global IPv4-specific BGP options to "address-family ipv4-unicast" + + # Move networks + network_path = ['protocols', 'bgp', asn, 'network'] + if config.exists(network_path): + config.set(bgp_path + af_path + ['network']) + config.set_tag(bgp_path + af_path + ['network']) + + networks = config.list_nodes(network_path) + for network in networks: + config.set(bgp_path + af_path + ['network', network]) + if config.exists(network_path + [network, 'route-map']): + route_map = config.return_value(network_path + [network, 'route-map']) + config.set(bgp_path + af_path + ['network', network, 'route-map'], value=route_map) + config.delete(network_path) + + # Move aggregate-address statements + aggregate_path = ['protocols', 'bgp', asn, 'aggregate-address'] + if config.exists(aggregate_path): + config.set(bgp_path + af_path + ['aggregate-address']) + config.set_tag(bgp_path + af_path + ['aggregate-address']) + + aggregates = config.list_nodes(aggregate_path) + for aggregate in aggregates: + config.set(bgp_path + af_path + ['aggregate-address', aggregate]) + if config.exists(aggregate_path + [aggregate, 'as-set']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'as-set']) + if config.exists(aggregate_path + [aggregate, 'summary-only']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'summary-only']) + config.delete(aggregate_path) + + ## Migrate neighbor options + neighbor_path = ['protocols', 'bgp', asn, 'neighbor'] + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + migrate_neighbor(config, neighbor_path, neighbor) + + peer_group_path = ['protocols', 'bgp', asn, 'peer-group'] + if config.exists(peer_group_path): + peer_groups = config.list_nodes(peer_group_path) + for peer_group in peer_groups: + migrate_neighbor(config, peer_group_path, peer_group) + + ## Migrate redistribute statements + redistribute_path = ['protocols', 'bgp', asn, 'redistribute'] + if config.exists(redistribute_path): + config.set(bgp_path + af_path + ['redistribute']) + + redistributes = config.list_nodes(redistribute_path) + for redistribute in redistributes: + config.set(bgp_path + af_path + ['redistribute', redistribute]) + if config.exists(redistribute_path + [redistribute, 'metric']): + redist_metric = config.return_value(redistribute_path + [redistribute, 'metric']) + config.set(bgp_path + af_path + ['redistribute', redistribute, 'metric'], value=redist_metric) + if config.exists(redistribute_path + [redistribute, 'route-map']): + redist_route_map = config.return_value(redistribute_path + [redistribute, 'route-map']) + config.set(bgp_path + af_path + ['redistribute', redistribute, 'route-map'], value=redist_route_map) + + config.delete(redistribute_path) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/ssh/0-to-1 b/src/migration-scripts/ssh/0-to-1 new file mode 100755 index 000000000..91b832276 --- /dev/null +++ b/src/migration-scripts/ssh/0-to-1 @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Delete "service ssh allow-root" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'ssh', 'allow-root']): + # Nothing to do + sys.exit(0) +else: + # Delete node with abandoned command + config.delete(['service', 'ssh', 'allow-root']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/10-to-11 b/src/migration-scripts/system/10-to-11 new file mode 100755 index 000000000..e6467243c --- /dev/null +++ b/src/migration-scripts/system/10-to-11 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Unclutter RADIUS configuiration for 'system login radius-server' +# Move radius-server top level tag nodes to a regular node which allows us +# to specify additional general features for the RADIUS client + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +cfg_base = ['system', 'login'] +if not config.exists(cfg_base + ['radius-server']): + # Nothing to do + sys.exit(0) +else: + # Migrate "system login radius-server" tag node to new + # "system login radius server" tag node and also rename the "secret" node to "key" + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + # "key" node is mandatory + key = config.return_value(base_server + ['secret']) + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # "port" is optional + if config.exists(base_server + ['port']): + port = config.return_value(base_server + ['port']) + config.set(cfg_base + ['radius', 'server', server, 'port'], value=port) + + # "timeout is optional" + if config.exists(base_server + ['timeout']): + timeout = config.return_value(base_server + ['timeout']) + config.set(cfg_base + ['radius', 'server', server, 'timeout'], value=timeout) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete old configuration node + config.delete(base_server) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/8-to-9 b/src/migration-scripts/system/8-to-9 new file mode 100755 index 000000000..db3fefdea --- /dev/null +++ b/src/migration-scripts/system/8-to-9 @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Deletes "system package" option as it is deprecated + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'package']): + # Nothing to do + sys.exit(0) +else: + # Delete the node with the old syntax + config.delete(['system', 'package']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/9-to-10 b/src/migration-scripts/system/9-to-10 new file mode 100755 index 000000000..c4343ec55 --- /dev/null +++ b/src/migration-scripts/system/9-to-10 @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +# converts opertator accts. to admin level accts. + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'login', 'user']): + # Nothing to do, which shouldn't happen anyway + # only if you wipe the config and reboot. + sys.exit(0) +else: + for usr in config.list_nodes(['system', 'login', 'user']): + if config.return_value(['system', 'login', 'user', usr, 'level']) == 'operator': + config.set(['system', 'login', 'user', usr, 'level'], value="admin", replace=True) + + try: + open(file_name,'w').write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + diff --git a/src/migration-scripts/webproxy/1-to-2 b/src/migration-scripts/webproxy/1-to-2 new file mode 100755 index 000000000..070ff356d --- /dev/null +++ b/src/migration-scripts/webproxy/1-to-2 @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# migrate old style `webproxy proxy-bypass 1.2.3.4/24` +# to new style `webproxy whitelist destination-address 1.2.3.4/24` + +import sys + +from vyos.configtree import ConfigTree + +if len(sys.argv) < 1: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_webproxy_base = ['service', 'webproxy'] +if not config.exists(cfg_webproxy_base + ['proxy-bypass']): + # Nothing to do + sys.exit(0) +else: + bypass_addresses = config.return_values(cfg_webproxy_base + ['proxy-bypass']) + # delete old configuration node + config.delete(cfg_webproxy_base + ['proxy-bypass']) + for bypass_address in bypass_addresses: + # add data to new configuration node + config.set(cfg_webproxy_base + ['whitelist', 'destination-address'], value=bypass_address, replace=False) + + # save updated configuration + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py index 7324c75ad..cfd321522 100755 --- a/src/op_mode/cpu_summary.py +++ b/src/op_mode/cpu_summary.py @@ -1,10 +1,22 @@ #!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. import re - from vyos.util import colon_separated_to_dict - FILE_NAME = '/proc/cpuinfo' with open(FILE_NAME, 'r') as f: diff --git a/src/op_mode/dynamic_dns_status.py b/src/op_mode/dynamic_dns.py index bbff01f49..0d457e247 100755 --- a/src/op_mode/dynamic_dns_status.py +++ b/src/op_mode/dynamic_dns.py @@ -1,5 +1,21 @@ #!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import os +import argparse import jinja2 import sys import time @@ -18,7 +34,7 @@ update-status: {{ entry.status }} {% endfor -%} """ -if __name__ == '__main__': +def show_status(): # Do nothing if service is not configured c = Config() if not c.exists_effective('service dns dynamic'): @@ -65,3 +81,26 @@ if __name__ == '__main__': tmpl = jinja2.Template(OUT_TMPL_SRC) print(tmpl.render(data)) + + +def update_ddns(): + os.system('systemctl stop ddclient') + os.remove(cache_file) + os.system('systemctl start ddclient') + + +def main(): + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument("--status", help="Show DDNS status", action="store_true") + group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") + args = parser.parse_args() + + if args.status: + show_status() + elif args.update: + update_ddns() + + +if __name__ == '__main__': + main() diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py index 7d8aefc91..847b543e0 100755 --- a/src/op_mode/maya_date.py +++ b/src/op_mode/maya_date.py @@ -27,17 +27,16 @@ class MayaDate(object): It represents the number of days passed since some date in the past the Maya believed is the day our world was created. - + Tzolkin calendar is for religious purposes, it has two independent cycles of 13 and 20 days, where 13 day cycle days are numbered, and 20 day cycle days are named. - + Haab calendar is for agriculture and daily life, it's a 365 day calendar with 18 months 20 days each, and 5 nameless days. The smallest unit of the long count calendar is one day (kin). - """ """ The long count calendar uses five different base 18 or base 20 @@ -131,7 +130,7 @@ class MayaDate(object): """ Seconds in day, for conversion from timestamp """ seconds_in_day = 60 * 60 * 24 - + def __init__(self, timestamp): if timestamp is None: self.days = self.start_days diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index f73d6c005..2f6112fb7 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -1,142 +1,175 @@ #!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + import os import sys import argparse -from datetime import datetime, timedelta, time as type_time, date as type_date -from subprocess import check_output,CalledProcessError,STDOUT +import subprocess import re +from datetime import datetime, timedelta, time as type_time, date as type_date +from subprocess import check_output, CalledProcessError, STDOUT + def yn(msg, default=False): - default_msg = "[Y/n]" if default else "[y/N]" - while True: - sys.stdout.write("%s %s " % (msg,default_msg)) - c = input().lower() - if c == '': - return default - elif c in ("y", "ye","yes"): - return True - elif c in ("n", "no"): - return False - else: - sys.stdout.write("Please respond with yes/y or no/n\n") + default_msg = "[Y/n]" if default else "[y/N]" + while True: + sys.stdout.write("%s %s " % (msg,default_msg)) + c = input().lower() + if c == '': + return default + elif c in ("y", "ye","yes"): + return True + elif c in ("n", "no"): + return False + else: + sys.stdout.write("Please respond with yes/y or no/n\n") def valid_time(s): - try: - return datetime.strptime(s, "%H:%M").time() - except ValueError: - return None + try: + return datetime.strptime(s, "%H:%M").time() + except ValueError: + return None def valid_date(s): + try: + return datetime.strptime(s, "%d%m%Y").date() + except ValueError: try: - return datetime.strptime(s, "%d%m%Y").date() + return datetime.strptime(s, "%d/%m/%Y").date() except ValueError: + try: + return datetime.strptime(s, "%d.%m.%Y").date() + except ValueError: try: - return datetime.strptime(s, "%d/%m/%Y").date() + return datetime.strptime(s, "%d:%m:%Y").date() except ValueError: - try: - return datetime.strptime(s, "%d.%m.%Y").date() - except ValueError: - try: - return datetime.strptime(s, "%d:%m:%Y").date() - except ValueError: - return None + return None def check_shutdown(): - try: - cmd = check_output(["/bin/systemctl","status","systemd-shutdownd.service"]) - #Shutodwn is scheduled - r = re.findall(r'Status: \"(.*)\"\n', cmd.decode())[0] - print(r) - except CalledProcessError as e: - #Shutdown is not scheduled - print("Shutdown is not scheduled") + try: + cmd = check_output(["/bin/systemctl","status","systemd-shutdownd.service"]) + #Shutodwn is scheduled + r = re.findall(r'Status: \"(.*)\"\n', cmd.decode())[0] + print(r) + except CalledProcessError as e: + #Shutdown is not scheduled + print("Shutdown is not scheduled") def cancel_shutdown(): - try: - cmd = check_output(["/sbin/shutdown","-c"]) - except CalledProcessError as e: - sys.exit("Error aborting shutdown: %s" % e) + try: + cmd = check_output(["/sbin/shutdown","-c"]) + except CalledProcessError as e: + sys.exit("Error aborting shutdown: %s" % e) def execute_shutdown(time, reboot = True, ask=True): - if not ask: - action = "reboot" if reboot else "poweroff" - if not yn("Are you sure you want to %s this system?" % action): - sys.exit(0) - - action = "-r" if reboot else "-P" - - if len(time) == 0: - cmd = check_output(["/sbin/shutdown",action,"now"],stderr=STDOUT) - print(cmd.decode().split(",",1)[0]) - return - - # Try to extract date from the first argument - if len(time) == 1: - time = time[0].split(" ",1) - - if len(time) == 1: - ts=valid_time(time[0]) - if time[0].isdigit() or valid_time(time[0]): - cmd = check_output(["/sbin/shutdown",action,time[0]],stderr=STDOUT) - else: - sys.exit("Timestamp needs to be in format of 12:34") - - elif len(time) == 2: - ts = valid_time(time[0]) - ds = valid_date(time[1]) - if ts and ds: - t = datetime.combine(ds, ts) - td = t-datetime.now() - t2 = 1+int(td.total_seconds())//60 # Get total minutes - cmd = check_output(["/sbin/shutdown",action,str(t2)],stderr=STDOUT) - else: - sys.exit("Timestamp needs to be in format of 12:34\nDatestamp in the format of DD.MM.YY") - else: - sys.exit("Could not decode time and date") + if not ask: + action = "reboot" if reboot else "poweroff" + if not yn("Are you sure you want to %s this system?" % action): + sys.exit(0) + + action = "-r" if reboot else "-P" + if len(time) == 0: + ### T870 legacy reboot job support + chk_vyatta_based_reboots() + ### + + cmd = check_output(["/sbin/shutdown",action,"now"],stderr=STDOUT) print(cmd.decode().split(",",1)[0]) + return -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--yes", "-y", - help="dont as for shutdown", - action="store_true", - dest="yes") - action = parser.add_mutually_exclusive_group(required=True) - action.add_argument("--reboot", "-r", - help="Reboot the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--poweroff", "-p", - help="Poweroff the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--cancel", "-c", - help="Cancel pending shutdown", - action="store_true") - - action.add_argument("--check", - help="Check pending chutdown", - action="store_true") - args = parser.parse_args() + # Try to extract date from the first argument + if len(time) == 1: + time = time[0].split(" ",1) - try: - if args.reboot: - execute_shutdown(args.reboot, reboot=True, ask=args.yes) - if args.poweroff: - execute_shutdown(args.poweroff, reboot=False,ask=args.yes) - if args.cancel: - cancel_shutdown() - if args.check: - check_shutdown() - except KeyboardInterrupt: - sys.exit("Interrupted") + if len(time) == 1: + ts = valid_time(time[0]) + if time[0].isdigit() or valid_time(time[0]): + cmd = check_output(["/sbin/shutdown",action,time[0]],stderr=STDOUT) + else: + sys.exit("Timestamp needs to be in format of 12:34") + + elif len(time) == 2: + ts = valid_time(time[0]) + ds = valid_date(time[1]) + if ts and ds: + t = datetime.combine(ds, ts) + td = t - datetime.now() + t2 = 1 + int(td.total_seconds())//60 # Get total minutes + cmd = check_output(["/sbin/shutdown",action,str(t2)],stderr=STDOUT) + else: + sys.exit("Timestamp needs to be in format of 12:34\nDatestamp in the format of DD.MM.YY") + else: + sys.exit("Could not decode time and date") + + print(cmd.decode().split(",",1)[0]) + +def chk_vyatta_based_reboots(): + ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed + ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job + ### name is the node of scheduled the job, commit-confirm checks for that + + f = r'/var/run/confirm.job' + if os .path.exists(f): + jid = open(f).read().strip() + if jid != 0: + subprocess.call(['sudo', 'atrm', jid]) + os.remove(f) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--yes", "-y", + help="dont as for shutdown", + action="store_true", + dest="yes") + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--reboot", "-r", + help="Reboot the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--poweroff", "-p", + help="Poweroff the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--cancel", "-c", + help="Cancel pending shutdown", + action="store_true") + + action.add_argument("--check", + help="Check pending chutdown", + action="store_true") + args = parser.parse_args() + + try: + if args.reboot is not None: + execute_shutdown(args.reboot, reboot=True, ask=args.yes) + if args.poweroff is not None: + execute_shutdown(args.poweroff, reboot=False,ask=args.yes) + if args.cancel: + cancel_shutdown() + if args.check: + check_shutdown() + except KeyboardInterrupt: + sys.exit("Interrupted") if __name__ == "__main__": - main() + main() diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py new file mode 100755 index 000000000..ab02d1eb3 --- /dev/null +++ b/src/op_mode/restart_dhcp_relay.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# File: restart_dhcp_relay.py +# Purpose: +# Restart IPv4 and IPv6 DHCP relay instances of dhcrelay service + +import sys +import argparse +import os + +import vyos.config + +parser = argparse.ArgumentParser() +parser.add_argument("--ipv4", action="store_true", help="Restart IPv4 DHCP relay") +parser.add_argument("--ipv6", action="store_true", help="Restart IPv6 DHCP relay") + +if __name__ == '__main__': + args = parser.parse_args() + c = vyos.config.Config() + + if args.ipv4: + # Do nothing if service is not configured + if not c.exists_effective('service dhcp-relay'): + print("DHCP relay service not configured") + else: + os.system('sudo systemctl restart isc-dhcp-relay.service') + + sys.exit(0) + elif args.ipv6: + # Do nothing if service is not configured + if not c.exists_effective('service dhcpv6-relay'): + print("DHCPv6 relay service not configured") + else: + os.system('sudo systemctl restart isc-dhcpv6-relay.service') + + sys.exit(0) + else: + parser.print_help() + sys.exit(1) diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py new file mode 100755 index 000000000..4c4ee6355 --- /dev/null +++ b/src/op_mode/show_dhcp.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import json +import argparse +import ipaddress +import tabulate +import sys + +from vyos.config import Config +from isc_dhcp_leases import Lease, IscDhcpLeases + +lease_file = "/config/dhcpd.leases" +pool_key = "shared-networkname" + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +def get_lease_data(lease): + data = {} + + # End time may not be present in backup leases + try: + data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + except: + data["expires"] = "" + + data["hardware_address"] = lease.ethernet + data["hostname"] = lease.hostname + data["ip"] = lease.ip + + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + + return data + +def get_leases(leases, state=None, pool=None): + leases = IscDhcpLeases(lease_file).get() + + if state is not None: + leases = list(filter(lambda x: x.binding_state == 'active', leases)) + + if pool is not None: + leases = list(filter(lambda x: in_pool(x, pool), leases)) + + return list(map(get_lease_data, leases)) + +def show_leases(leases): + headers = ["IP address", "Hardware address", "Lease expiration", "Pool", "Client Name"] + + lease_list = [] + for l in leases: + lease_list.append([l["ip"], l["hardware_address"], l["expires"], l["pool"], l["hostname"]]) + + output = tabulate.tabulate(lease_list, headers) + + print(output) + +def get_pool_size(config, pool): + size = 0 + subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool)) + for s in subnets: + ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s)) + for r in ranges: + start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) + stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) + + size += int(ipaddress.IPv4Address(stop)) - int(ipaddress.IPv4Address(start)) + + return size + +def show_pool_stats(stats): + headers = ["Pool", "Size", "Leases", "Available", "Usage"] + output = tabulate.tabulate(stats, headers) + + print(output) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + + parser.add_argument("-e", "--expired", action="store_true", help="Show expired leases") + parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + + args = parser.parse_args() + + # Do nothing if service is not configured + config = Config() + if not config.exists_effective('service dhcp-server'): + print("DHCP service is not configured") + sys.exit(0) + + if args.leases: + if args.expired: + if args.pool: + leases = get_leases(lease_file, state='free', pool=args.pool) + else: + leases = get_leases(lease_file, state='free') + else: + if args.pool: + leases = get_leases(lease_file, state='active', pool=args.pool) + else: + leases = get_leases(lease_file, state='active') + + if args.json: + print(json.dumps(leases, indent=4)) + else: + show_leases(leases) + elif args.statistics: + pools = [] + + # Get relevant pools + if args.pool: + pools = [args.pool] + else: + pools = config.list_effective_nodes("service dhcp-server shared-network-name") + + # Get pool usage stats + stats = [] + for p in pools: + size = get_pool_size(config, p) + leases = len(get_leases(lease_file, state='active', pool=args.pool)) + + if size != 0: + use_percentage = round(leases / size) * 100 + else: + use_percentage = 0 + + if args.json: + pool_stats = {"pool": p, "size": size, "leases": leases, + "available": (size - leases), "percentage": use_percentage} + else: + # For tabulate + pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)] + stats.append(pool_stats) + + # Print stats + if args.json: + print(json.dumps(stats, indent=4)) + else: + show_pool_stats(stats) + else: + print("Use either --leases or --statistics option") diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py new file mode 100755 index 000000000..bf73b92ea --- /dev/null +++ b/src/op_mode/show_dhcpv6.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import json +import argparse +import ipaddress +import tabulate +import sys + +from vyos.config import Config +from isc_dhcp_leases import Lease, IscDhcpLeases + +lease_file = "/config/dhcpdv6.leases" + +def get_lease_data(lease): + data = {} + + # End time may not be present in backup leases + try: + data["expires"] = lease.end.strftime("%Y/%m/%d %H:%M:%S") + except: + data["expires"] = "" + + data["duid"] = lease.host_identifier_string + data["ip"] = lease.ip + + return data + +def get_leases(leases, state=None): + leases = IscDhcpLeases(lease_file).get() + + if state is not None: + leases = list(filter(lambda x: x.binding_state == 'active', leases)) + + return list(map(get_lease_data, leases)) + +def show_leases(leases): + headers = ["IPv6 address", "Lease expiration", "DUID"] + + lease_list = [] + for l in leases: + lease_list.append([l["ip"], l["expires"], l["duid"]]) + + output = tabulate.tabulate(lease_list, headers) + + print(output) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + + parser.add_argument("-p", "--pool", type=str, action="store", help="Show lease for specific pool") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Product JSON output") + + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service dhcpv6-server'): + print("DHCPv6 service is not configured") + sys.exit(0) + + if args.leases: + leases = get_leases(lease_file, state='active') + show_leases(leases) + elif args.statistics: + print("DHCPv6 statistics option is not available") + else: + print("Invalid option") diff --git a/src/op_mode/show_igmpproxy.py b/src/op_mode/show_igmpproxy.py new file mode 100755 index 000000000..5ccc16287 --- /dev/null +++ b/src/op_mode/show_igmpproxy.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# File: show_igmpproxy.py +# Purpose: +# Display istatistics from IPv4 IGMP proxy. +# Used by the "run show ip multicast" command tree. + +import sys +import jinja2 +import argparse +import ipaddress +import socket + +import vyos.config + +# Output Template for "show ip multicast interface" command +# +# Example: +# Interface BytesIn PktsIn BytesOut PktsOut Local +# eth0 0.0b 0 0.0b 0 xxx.xxx.xxx.65 +# eth1 0.0b 0 0.0b 0 xxx.xxx.xx.201 +# eth0.3 0.0b 0 0.0b 0 xxx.xxx.x.7 +# tun1 0.0b 0 0.0b 0 xxx.xxx.xxx.2 +vif_out_tmpl = """ +{%- for r in data %} +{{ "%-10s"|format(r.interface) }} {{ "%-12s"|format(r.bytes_in) }} {{ "%-12s"|format(r.pkts_in) }} {{ "%-12s"|format(r.bytes_out) }} {{ "%-12s"|format(r.pkts_out) }} {{ "%-15s"|format(r.loc) }} +{%- endfor %} +""" + +# Output Template for "show ip multicast mfc" command +# +# Example: +# Group Origin In Out Pkts Bytes Wrong +# xxx.xxx.xxx.250 xxx.xx.xxx.75 -- +# xxx.xxx.xx.124 xx.xxx.xxx.26 -- +mfc_out_tmpl = """ +{%- for r in data %} +{{ "%-15s"|format(r.group) }} {{ "%-15s"|format(r.origin) }} {{ "%-12s"|format(r.pkts) }} {{ "%-12s"|format(r.bytes) }} {{ "%-12s"|format(r.wrong) }} {{ "%-10s"|format(r.iif) }} {{ "%-20s"|format(r.oifs|join(', ')) }} +{%- endfor %} +""" + +parser = argparse.ArgumentParser() +parser.add_argument("--interface", action="store_true", help="Interface Statistics") +parser.add_argument("--mfc", action="store_true", help="Multicast Forwarding Cache") + +def byte_string(size): + # convert size to integer + size = int(size) + + # One Terrabyte + s_TB = 1024 * 1024 * 1024 * 1024 + # One Gigabyte + s_GB = 1024 * 1024 * 1024 + # One Megabyte + s_MB = 1024 * 1024 + # One Kilobyte + s_KB = 1024 + # One Byte + s_B = 1 + + if size > s_TB: + return str(round((size/s_TB), 2)) + 'TB' + elif size > s_GB: + return str(round((size/s_GB), 2)) + 'GB' + elif size > s_MB: + return str(round((size/s_MB), 2)) + 'MB' + elif size > s_KB: + return str(round((size/s_KB), 2)) + 'KB' + else: + return str(round((size/s_B), 2)) + 'b' + + return None + +def kernel2ip(addr): + """ + Convert any given addr from Linux Kernel to a proper, IPv4 address + using the correct host byte order. + """ + + # Convert from hex 'FE000A0A' to decimal '4261415434' + addr = int(addr, 16) + # Kernel ABI _always_ uses network byteorder + addr = socket.ntohl(addr) + + return ipaddress.IPv4Address( addr ) + +def do_mr_vif(): + """ + Read contents of file /proc/net/ip_mr_vif and print a more human + friendly version to the command line. IPv4 addresses present as + 32bit integers in hex format are converted to IPv4 notation, too. + """ + + with open('/proc/net/ip_mr_vif', 'r') as f: + lines = len(f.readlines()) + if lines < 2: + return None + + result = { + 'data': [] + } + + # Build up table format string + table_format = { + 'interface': 'Interface', + 'pkts_in' : 'PktsIn', + 'pkts_out' : 'PktsOut', + 'bytes_in' : 'BytesIn', + 'bytes_out': 'BytesOut', + 'loc' : 'Local' + } + result['data'].append(table_format) + + # read and parse information from /proc filesystema + with open('/proc/net/ip_mr_vif', 'r') as f: + header_line = next(f) + for line in f: + data = { + 'interface': line.split()[1], + 'pkts_in' : line.split()[3], + 'pkts_out' : line.split()[5], + + # convert raw byte number to something more human readable + # Note: could be replaced by Python3 hurry.filesize module + 'bytes_in' : byte_string( line.split()[2] ), + 'bytes_out': byte_string( line.split()[4] ), + + # convert IP address from hex 'FE000A0A' to decimal '4261415434' + 'loc' : kernel2ip( line.split()[7] ), + } + result['data'].append(data) + + return result + +def do_mr_mfc(): + """ + Read contents of file /proc/net/ip_mr_cache and print a more human + friendly version to the command line. IPv4 addresses present as + 32bit integers in hex format are converted to IPv4 notation, too. + """ + + with open('/proc/net/ip_mr_cache', 'r') as f: + lines = len(f.readlines()) + if lines < 2: + return None + + # We need this to convert from interface index to a real interface name + # Thus we also skip the format identifier on list index 0 + vif = do_mr_vif()['data'][1:] + + result = { + 'data': [] + } + + # Build up table format string + table_format = { + 'group' : 'Group', + 'origin': 'Origin', + 'iif' : 'In', + 'oifs' : ['Out'], + 'pkts' : 'Pkts', + 'bytes' : 'Bytes', + 'wrong' : 'Wrong' + } + result['data'].append(table_format) + + # read and parse information from /proc filesystem + with open('/proc/net/ip_mr_cache', 'r') as f: + header_line = next(f) + for line in f: + data = { + # convert IP address from hex 'FE000A0A' to decimal '4261415434' + 'group' : kernel2ip( line.split()[0] ), + 'origin': kernel2ip( line.split()[1] ), + + 'iif' : '--', + 'pkts' : '', + 'bytes' : '', + 'wrong' : '', + 'oifs' : [] + } + + iif = int( line.split()[2] ) + if not ((iif == -1) or (iif == 65535)): + data['pkts'] = line.split()[3] + data['bytes'] = byte_string( line.split()[4] ) + data['wrong'] = line.split()[5] + + # convert index to real interface name + data['iif'] = vif[iif]['interface'] + + # convert each output interface index to a real interface name + for oif in line.split()[6:]: + idx = int( oif.split(':')[0] ) + data['oifs'].append( vif[idx]['interface'] ) + + result['data'].append(data) + + return result + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = vyos.config.Config() + if not c.exists_effective('protocols igmp-proxy'): + print("IGMP proxy is not configured") + sys.exit(0) + + if args.interface: + data = do_mr_vif() + if data: + tmpl = jinja2.Template(vif_out_tmpl) + print(tmpl.render(data)) + + sys.exit(0) + elif args.mfc: + data = do_mr_mfc() + if data: + tmpl = jinja2.Template(mfc_out_tmpl) + print(tmpl.render(data)) + + sys.exit(0) + else: + parser.print_help() + sys.exit(1) + diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py new file mode 100755 index 000000000..0828743e8 --- /dev/null +++ b/src/op_mode/show_ipsec_sa.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +import vici +import tabulate +import hurry.filesize + +import vyos.util + + +try: + session = vici.Session() + sas = session.list_sas() +except PermissionError: + print("You do not have a permission to connect to the IPsec daemon") + sys.exit(1) +except ConnectionRefusedError: + print("IPsec is not runing") + sys.exit(1) +except Exception as e: + print("An error occured: {0}".format(e)) + sys.exit(1) + +sa_data = [] + +for sa in sas: + # list_sas() returns a list of single-item dicts + for peer in sa: + parent_sa = sa[peer] + + if parent_sa["state"] == b"ESTABLISHED": + state = "up" + else: + state = "down" + + if state == "up": + uptime = vyos.util.seconds_to_human(parent_sa["established"].decode()) + else: + uptime = "N/A" + + remote_host = parent_sa["remote-host"].decode() + remote_id = parent_sa["remote-id"].decode() + + if remote_host == remote_id: + remote_id = "N/A" + + # The counters can only be obtained from the child SAs + child_sas = parent_sa["child-sas"] + installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} + + if not installed_sas: + data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] + sa_data.append(data) + else: + for csa in installed_sas: + isa = installed_sas[csa] + + bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) + bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) + bytes_str = "{0}/{1}".format(bytes_in, bytes_out) + + pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) + pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) + pkts_str = "{0}/{1}".format(pkts_in, pkts_out) + # Remove B from <1K values + pkts_str = re.sub(r'B', r'', pkts_str) + + enc = isa["encr-alg"].decode() + key_size = isa["encr-keysize"].decode() + hash = isa["integ-alg"].decode() + if "dh-group" in isa: + dh_group = isa["dh-group"].decode() + else: + dh_group = "" + proposal = "{0}_{1}/{2}".format(enc, key_size, hash) + if dh_group: + proposal = "{0}/{1}".format(proposal, dh_group) + + data = [peer, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + sa_data.append(data) + +headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] +output = tabulate.tabulate(sa_data, headers) +print(output) diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py new file mode 100755 index 000000000..886d94f16 --- /dev/null +++ b/src/op_mode/system_integrity.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import subprocess +import re +import itertools +from datetime import datetime, timedelta + +verf = r'/usr/libexec/vyos/op_mode/version.py' + +def get_sys_build_version(): + if not os.path.exists(verf): + return None + + a = subprocess.check_output(['/usr/libexec/vyos/op_mode/version.py']).decode() + if re.search('^Built on:.+',a, re.M) == None: + return None + + dt = ( re.sub('Built on: +','', re.search('^Built on:.+',a, re.M).group(0)) ) + return datetime.strptime(dt,'%a %d %b %Y %H:%M %Z') + +def check_pkgs(dt): + pkg_diffs = { + 'buildtime' : str(dt), + 'pkg' : {} + } + + pkg_info = os.listdir('/var/lib/dpkg/info/') + for file in pkg_info: + if re.search('\.list$', file): + fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime + dt_str = (datetime.utcfromtimestamp(fts).strftime('%Y-%m-%d %H:%M:%S')) + fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') + if fdt > dt: + pkg_diffs['pkg'].update( { str(re.sub('\.list','',file)) : str(fdt)}) + + if len(pkg_diffs['pkg']) != 0: + return pkg_diffs + else: + return None + +def main(): + dt = get_sys_build_version() + pkgs = check_pkgs(dt) + if pkgs != None: + print ("The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime']) + for k, v in pkgs['pkg'].items(): + print ("installed: " + v + '\t' + k) + +if __name__ == '__main__': + sys.exit( main() ) + diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py new file mode 100755 index 000000000..66622c04c --- /dev/null +++ b/src/op_mode/wireguard.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import argparse +import os +import sys +import subprocess +import syslog as sl + +from vyos import ConfigError + +dir = r'/config/auth/wireguard' +pk = dir + '/private.key' +pub = dir + '/public.key' +psk = dir + '/preshared.key' + +def check_kmod(): + """ check if kmod is loaded, if not load it """ + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") + +def generate_keypair(): + """ generates a keypair which is stored in /config/auth/wireguard """ + ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) + if ret != 0: + raise ConfigError("wireguard key-pair generation failed") + else: + sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) + +def genkey(): + """ helper function to check, regenerate the keypair """ + old_umask = os.umask(0o077) + if os.path.exists(pk) and os.path.exists(pub): + try: + choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ") + if choice == 'y' or choice == 'Y': + generate_keypair() + except KeyboardInterrupt: + sys.exit(0) + else: + """ if keypair is bing executed from a running iso """ + if not os.path.exists(dir): + os.umask(old_umask) + subprocess.call(['sudo mkdir -p ' + dir], shell=True) + subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True) + subprocess.call(['sudo chmod 770 ' + dir], shell=True) + generate_keypair() + os.umask(old_umask) + +def showkey(key): + """ helper function to show privkey or pubkey """ + if key == "pub": + if os.path.exists(pub): + print ( open(pub).read().strip() ) + else: + print("no public key found") + + if key == "pk": + if os.path.exists(pk): + print ( open(pk).read().strip() ) + else: + print("no private key found") + +def genpsk(): + """ generates a preshared key and shows it on stdout, it's stroed only in the config """ + subprocess.call(['wg genpsk'], shell=True) + +if __name__ == '__main__': + check_kmod() + + parser = argparse.ArgumentParser(description='wireguard key management') + parser.add_argument('--genkey', action="store_true", help='generate key-pair') + parser.add_argument('--showpub', action="store_true", help='shows public key') + parser.add_argument('--showpriv', action="store_true", help='shows private key') + parser.add_argument('--genpsk', action="store_true", help='generates preshared-key') + args = parser.parse_args() + + try: + if args.genkey: + genkey() + if args.showpub: + showkey("pub") + if args.showpriv: + showkey("pk") + if args.genpsk: + genpsk() + + except ConfigError as e: + print(e) + sys.exit(1) + diff --git a/src/system/normalize-ip b/src/system/normalize-ip new file mode 100755 index 000000000..08f922a8e --- /dev/null +++ b/src/system/normalize-ip @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +# Normalizes IPv6 addresses so that they can be passed to iproute2, +# since iproute2 will not take an address with leading zeroes for an argument + +import re +import sys +import ipaddress + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Argument required") + sys.exit(1) + + address_string, prefix_length = re.match(r'(.+)/(.+)', sys.argv[1]).groups() + + try: + address = ipaddress.IPv6Address(address_string) + normalized_address = address.compressed + except ipaddress.AddressValueError: + # It's likely an IPv4 address, do nothing + normalized_address = address_string + + print("{0}/{1}".format(normalized_address, prefix_length)) + sys.exit(0) + diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh new file mode 100755 index 000000000..02bbd4c3c --- /dev/null +++ b/src/system/on-dhcp-event.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# This script came from ubnt.com forum user "bradd" in the following post +# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311 +# It has been modified by Ubiquiti to update the /etc/host file +# instead of adding to the CLI. +# Thanks to forum user "itsmarcos" for bug fix & improvements +# Thanks to forum user "ruudboon" for multiple domain fix +# Thanks to forum user "chibby85" for expire patch and static-mapping + +if [ $# -lt 5 ]; then + echo Invalid args + logger -s -t on-dhcp-event "Invalid args \"$@\"" + exit 1 +fi + +action=$1 +client_name=$2 +client_ip=$3 +client_mac=$4 +domain=$5 +file=/etc/hosts +changes=0 + +if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "client-"$client_mac | tr : -) +fi + +if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name +else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" +fi + +case "$action" in + commit) # add mapping for new lease + echo "- new lease event, setting static mapping for host "\ + "$client_fqdn_name (MAC=$client_mac, IP=$client_ip)" + # + # grep fails miserably with \t in the search expression. + # In the following line one <Ctrl-V> <TAB> is used after $client_search_expr + # followed by a single space + grep -q " $client_search_expr #on-dhcp-event " $file + if [ $? == 0 ]; then + echo pattern found, removing + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + fi + else + echo pattern NOT found + fi + + # check if hostname already exists (e.g. a static host mapping) + # if so don't overwrite + grep -q " $client_search_expr " $file + if [ $? == 0 ]; then + echo host $client_fqdn_name already exists, exiting + exit 1 + fi + + line="$client_ip\t $client_fqdn_name\t #on-dhcp-event $client_mac" + sudo sh -c "echo -e '$line' >> $file" + ((changes++)) + echo Entry was added + ;; + + release) # delete mapping for released address + echo "- lease release event, deleting static mapping for host $client_fqdn_name" + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + else + echo Entry was removed + ((changes++)) + fi + ;; + + *) + logger -s -t on-dhcp-event "Invalid command \"$1\"" + exit 1; + ;; +esac + +if [ $changes -gt 0 ]; then + echo Success + pid=`pgrep pdns_recursor` + if [ -n "$pid" ]; then + sudo rec_control reload-zones + fi +else + echo No changes made +fi +exit 0 + + diff --git a/src/system/post-upgrade b/src/system/post-upgrade new file mode 100755 index 000000000..41b7c01ba --- /dev/null +++ b/src/system/post-upgrade @@ -0,0 +1,3 @@ +#!/bin/sh + +chown -R root:vyattacfg /config diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service new file mode 100644 index 000000000..e5c289466 --- /dev/null +++ b/src/systemd/tftpd@.service @@ -0,0 +1,14 @@ +[Unit] +Description=TFTP server +After=network.target +RequiresMountsFor=/run + +[Service] +Type=forking +#NotifyAccess=main +EnvironmentFile=-/etc/default/tftpd%I +ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS" +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py new file mode 100644 index 000000000..e47770a7f --- /dev/null +++ b/src/tests/test_config_parser.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import tempfile +import unittest +from unittest import TestCase, mock + +import vyos.configtree + + +class TestConfigParser(TestCase): + def setUp(self): + with open('tests/data/config.valid', 'r') as f: + config_string = f.read() + self.config = vyos.configtree.ConfigTree(config_string) + + def test_top_level_valueless(self): + self.assertTrue(self.config.exists(["top-level-valueless-node"])) + + def test_top_level_leaf(self): + self.assertTrue(self.config.exists(["top-level-leaf-node"])) + self.assertEqual(self.config.return_value(["top-level-leaf-node"]), "foo") + + def test_top_level_tag(self): + self.assertTrue(self.config.exists(["top-level-tag-node"])) + # No sorting is intentional, child order must be preserved + self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["foo", "bar"]) + + def test_copy(self): + self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"]) + print(self.config.to_string()) + self.assertTrue(self.config.exists(["top-level-tag-node", "baz"])) + + def test_copy_duplicate(self): + with self.assertRaises(vyos.configtree.ConfigTreeError): + self.config.copy(["top-level-tag-node", "foo"], ["top-level-tag-node", "bar"]) + + def test_rename(self): + self.config.rename(["top-level-tag-node", "bar"], "quux") + print(self.config.to_string()) + self.assertTrue(self.config.exists(["top-level-tag-node", "quux"])) + + def test_rename_duplicate(self): + with self.assertRaises(vyos.configtree.ConfigTreeError): + self.config.rename(["top-level-tag-node", "foo"], "bar") diff --git a/src/tests/test_host_name.py b/src/tests/test_host_name.py deleted file mode 100644 index 8c5210d5f..000000000 --- a/src/tests/test_host_name.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# - -import os -import tempfile -import unittest -from unittest import TestCase, mock - -from vyos import ConfigError -try: - from src.conf_mode import host_name -except ModuleNotFoundError: # for unittest.main() - import sys - sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) - from src.conf_mode import host_name - - -class TestHostName(TestCase): - - def test_get_config(self): - tests = [ - {'name': 'empty_hostname_and_domain', - 'host-name': '', - 'domain-name': '', - 'expected': {"hostname": 'vyos', "domain": '', "fqdn": 'vyos'}}, - {'name': 'empty_hostname', - 'host-name': '', - 'domain-name': 'localdomain', - 'expected': {"hostname": 'vyos', "domain": 'localdomain', "fqdn": 'vyos.localdomain'}}, - {'name': 'has_hostname', - 'host-name': 'router', - 'domain-name': '', - 'expected': {"hostname": 'router', "domain": '', "fqdn": 'router'}}, - {'name': 'has_hostname_and_domain', - 'host-name': 'router', - 'domain-name': 'localdomain', - 'expected': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}}, - ] - for t in tests: - def mocked_return_value(path, default=None): - return t[path.split()[1]] - - with self.subTest(msg=t['name'], hostname=t['host-name'], domain=t['domain-name'], expected=t['expected']): - with mock.patch('vyos.config.Config.return_value', side_effect=mocked_return_value): - actual = host_name.get_config() - self.assertEqual(actual, t['expected']) - - - def test_verify(self): - tests = [ - {'name': 'valid_hostname', - 'config': {"hostname": 'vyos', "domain": 'localdomain', "fqdn": 'vyos.localdomain'}, - 'expected': None}, - {'name': 'invalid_hostname', - 'config': {"hostname": 'vyos..', "domain": '', "fqdn": ''}, - 'expected': ConfigError}, - {'name': 'invalid_hostname_length', - 'config': {"hostname": 'a'*64, "domain": '', "fqdn": ''}, - 'expected': ConfigError} - ] - for t in tests: - with self.subTest(msg=t['name'], config=t['config'], expected=t['expected']): - if t['expected'] is not None: - with self.assertRaises(t['expected']): - host_name.verify(t['config']) - else: - host_name.verify(t['config']) - - def test_generate(self): - tests = [ - {'name': 'has_old_entry', - 'has_old_entry': True, - 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, - 'expected': ['127.0.1.1', 'router.localdomain']}, - {'name': 'no_old_entry', - 'has_old_entry': False, - 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, - 'expected': ['127.0.1.1', 'router.localdomain']}, - ] - for t in tests: - with self.subTest(msg=t['name'], config=t['config'], has_old_entry=t['has_old_entry'], expected=t['expected']): - m = mock.MagicMock(return_value=b'debian') - with mock.patch('subprocess.check_output', m): - host_name.hosts_file = tempfile.mkstemp()[1] - if t['has_old_entry']: - with open(host_name.hosts_file, 'w') as f: - f.writelines(['\n127.0.1.1 {} # VyOS entry'.format('debian')]) - host_name.generate(t['config']) - if len(t['expected']) > 0: - self.assertTrue(os.path.isfile(host_name.hosts_file)) - with open(host_name.hosts_file) as f: - actual = f.read() - self.assertEqual( - t['expected'], actual.splitlines()[1].split()[0:2]) - os.remove(host_name.hosts_file) - else: - self.assertFalse(os.path.isfile(host_name.hosts_file)) - - - def test_apply(self): - tests = [ - {'name': 'valid_hostname', - 'config': {"hostname": 'router', "domain": 'localdomain', "fqdn": 'router.localdomain'}, - 'expected': [mock.call('hostnamectl set-hostname --static router.localdomain'), - mock.call('systemctl restart rsyslog.service')]} - ] - for t in tests: - with self.subTest(msg=t['name'], c=t['config'], expected=t['expected']): - with mock.patch('os.system') as os_system: - host_name.apply(t['config']) - os_system.assert_has_calls(t['expected']) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py index 023a30723..c4c59b827 100644 --- a/src/tests/test_initial_setup.py +++ b/src/tests/test_initial_setup.py @@ -25,7 +25,7 @@ import vyos.configtree import vyos.initialsetup as vis -class TestHostName(TestCase): +class TestInitialSetup(TestCase): def setUp(self): with open('tests/data/config.boot.default', 'r') as f: config_string = f.read() diff --git a/src/tests/test_ntp.py b/src/tests/test_ntp.py new file mode 100644 index 000000000..1cde490b4 --- /dev/null +++ b/src/tests/test_ntp.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import tempfile +import unittest +from unittest import TestCase, mock +import ipaddress +from contextlib import ExitStack +import textwrap + +from vyos import ConfigError +from vyos.config import Config +try: + from src.conf_mode import ntp +except ModuleNotFoundError: # for unittest.main() + import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + from src.conf_mode import ntp + + +class TestNtp(TestCase): + + def test_get_config(self): + tests = [ + { + 'name': 'empty', + 'config': { + 'system ntp': None, + }, + 'expected': None, + }, + { + 'name': 'full-options', + 'config': { + 'system ntp': 'yes', + 'allow-clients address': ['192.0.2.0/24'], + 'listen-address': ['198.51.100.0/24'], + 'server': ['example.com'], + 'server example.com noselect': 'yes', + 'server example.com preempt': 'yes', + 'server example.com prefer': 'yes', + }, + 'expected': { + 'allowed_networks': [{ + 'address': ipaddress.ip_address('192.0.2.0'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/24', + }], + 'listen_address': ['198.51.100.0/24'], + 'servers': [ + {'name': 'example.com', 'options': ['noselect', 'preempt', 'prefer']} + ] + }, + }, + { + 'name': 'non-options', + 'config': { + 'system ntp': 'yes', + 'allow-clients address': ['192.0.2.0/24'], + 'listen-address': ['198.51.100.0/24'], + 'server': ['example.com'], + }, + 'expected': { + 'allowed_networks': [{ + 'address': ipaddress.ip_address('192.0.2.0'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/24', + }], + 'listen_address': ['198.51.100.0/24'], + 'servers': [ + {'name': 'example.com', 'options': []} + ] + }, + }, + ] + for case in tests: + def mocked_fn(path): + return case['config'].get(path) + + with self.subTest(msg = case['name']): + m = { + 'return_value': mock.Mock(side_effect = mocked_fn), + 'return_values': mock.Mock(side_effect = mocked_fn), + 'list_nodes': mock.Mock(side_effect = mocked_fn), + 'exists': mock.Mock(side_effect = mocked_fn), + } + with mock.patch.multiple(Config, **m): + actual = ntp.get_config() + self.assertEqual(actual, case['expected']) + + def test_verify(self): + tests = [ + { + 'name': 'none', + 'config': None, + 'expected': None + }, + { + 'name': 'valid', + 'config': { + 'allowed_networks': [{ + 'address': ipaddress.ip_address('192.0.2.1'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/24', + }], + 'listen_address': ['198.51.100.0/24'], + 'servers': [ + {'name': 'example.com', 'options': ['noselect', 'preempt', 'prefer']} + ] + }, + 'expected': None, + }, + { + 'name': 'not configure servers', + 'config': { + 'allowed_networks': [{ + 'address': ipaddress.ip_address('192.0.2.1'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/24', + }], + 'servers': [] + }, + 'expected': ConfigError, + }, + { + 'name': 'does not exist in the network', + 'config': { + 'allowed_networks': [{ + 'address': ipaddress.ip_address('192.0.2.1'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/50', # invalid netmask + }], + 'listen_address': ['198.51.100.0/24'], + 'servers': [ + {'name': 'example.com', 'options': []} + ] + }, + 'expected': ConfigError, + }, + ] + for case in tests: + with self.subTest(msg = case['name']): + if case['expected'] is not None: + with self.assertRaises(case['expected']): + ntp.verify(case['config']) + else: + ntp.verify(case['config']) + + def test_generate(self): + tests = [ + { + 'name': 'empty', + 'config': None, + 'expected': '', + }, + { + 'name': 'valid', + 'config': { + 'allowed_networks': [ + { + 'address': ipaddress.ip_address('192.0.2.1'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '192.0.2.0/24', + }, + { + 'address': ipaddress.ip_address('198.51.100.1'), + 'netmask': ipaddress.ip_address('255.255.255.0'), + 'network': '198.51.100.0/24', + }, + ], + 'listen_address': ['198.51.100.0/24'], + 'servers': [ + {'name': '1.example.com', 'options': ['noselect', 'preempt', 'prefer']}, + {'name': '2.example.com', 'options': []}, + ] + }, + 'expected': textwrap.dedent(''' + ### Autogenerated by ntp.py ### + + # + # Non-configurable defaults + # + driftfile /var/lib/ntp/ntp.drift + # By default, only allow ntpd to query time sources, ignore any incoming requests + restrict default noquery nopeer notrap nomodify + # Local users have unrestricted access, allowing reconfiguration via ntpdc + restrict 127.0.0.1 + restrict -6 ::1 + + # + # Configurable section + # + + # Server configuration for: 1.example.com + server 1.example.com iburst noselect preempt prefer + # Server configuration for: 2.example.com + server 2.example.com iburst + + + # Client configuration for network: 192.0.2.0/24 + restrict 192.0.2.1 mask 255.255.255.0 nomodify notrap nopeer + + # Client configuration for network: 198.51.100.0/24 + restrict 198.51.100.1 mask 255.255.255.0 nomodify notrap nopeer + + + + # NTP should listen on configured addresses only + interface ignore wildcard + interface listen 198.51.100.0/24 + + '''), + }, + ] + + for case in tests: + with self.subTest(msg = case['name']): + with tempfile.NamedTemporaryFile() as fp: + ntp.config_file = fp.name + + ntp.generate(case['config']) + actual = fp.file.read().decode('ascii') + print(actual) + self.assertEqual(case['expected'], actual) + + def test_apply(self): + with tempfile.NamedTemporaryFile(delete = False) as fp: + ntp.config_file = fp.name + with mock.patch('os.system') as os_system: + ntp.apply({}) # some configure + os_system.assert_has_calls([ + mock.call('sudo systemctl restart ntp.service'), + ]) + self.assertTrue(os.path.exists(fp.name)) + + ntp.apply(None) # empty configure + os_system.assert_has_calls([ + mock.call('sudo systemctl stop ntp.service'), + ]) + self.assertFalse(os.path.exists(fp.name)) + +if __name__ == "__main__": + unittest.main() diff --git a/src/utils/vyos-config-to-commands b/src/utils/vyos-config-to-commands index 8b50f7c5d..7147bc5ff 100755 --- a/src/utils/vyos-config-to-commands +++ b/src/utils/vyos-config-to-commands @@ -19,6 +19,14 @@ else: except OSError as e: print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr) +# This script is usually called with the output of "cli-shell-api showCfg", which does not +# escape backslashes. "ConfigTree()" expects escaped backslashes when parsing a config +# string (and also prints them itself). Therefore this script would fail. +# Manually escape backslashes here to handle backslashes in any configuration strings +# properly. The alternative would be to modify the output of "cli-shell-api showCfg", +# but that may be break other things who rely on that specific output. +config_string = config_string.replace("\\", "\\\\") + try: config = ConfigTree(config_string) commands = config.to_commands() diff --git a/src/validators/mac-address b/src/validators/mac-address new file mode 100755 index 000000000..d6ec6d712 --- /dev/null +++ b/src/validators/mac-address @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import re + +if len(sys.argv) == 2: + pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$" + if re.match(pattern, sys.argv[1]): + sys.exit(0) + else: + sys.exit(1) + diff --git a/tests/data/config.valid b/tests/data/config.valid new file mode 100644 index 000000000..a21c6a4d1 --- /dev/null +++ b/tests/data/config.valid @@ -0,0 +1,39 @@ +/* top level leaf node */ +top-level-leaf-node foo + +top-level-valueless-node + +top-level-tag-node foo { + top-level-tag-node-child some-value +} + +top-level-tag-node bar { + top-level-tag-node-child another-value +} + +normal-node { + normal-node-child { + valueless-node + multi-node value1 + /* valueless node comment */ + another-valueless-node + multi-node value1 + tag-node foo { + } + one-more-valueless-node + tag-node bar { + some-option some-value + } + } + option-with-quoted-value "some-value" +} + +trailing-leaf-node-option some-value + +empty-node { +} + +trailing-leaf-node-without-value + +/* Trailing commend */ +/* Another trailing comment */ |